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,729 @@
|
|
|
1
|
+
"""The health of every phone: the alerts and pre-run checks the Phones page shows (Mobster for Mac › Phones)."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import errno
|
|
5
|
+
import fcntl
|
|
6
|
+
import functools
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .api_errors import APIError
|
|
17
|
+
from .devices import REASONS
|
|
18
|
+
|
|
19
|
+
_Thread = threading.Thread
|
|
20
|
+
|
|
21
|
+
CODES = ("unplugged", "locked", "signed_out", "challenge", "region_mismatch", "owner_active")
|
|
22
|
+
ATTENTION, INFO = "attention", "info"
|
|
23
|
+
SEVERITY = {code: ATTENTION for code in CODES} | {"owner_active": INFO}
|
|
24
|
+
STORE = "fleet-health.json"
|
|
25
|
+
DEBOUNCE_S = 20.0
|
|
26
|
+
# How long a phone's region, language and time zone stand before they are read again. The pre-run check reads again
|
|
27
|
+
# after PRE_RUN_TTL, so a person who just changed a setting isn't refused on the old value for long.
|
|
28
|
+
LOCALE_TTL = 300.0
|
|
29
|
+
PRE_RUN_TTL = 5.0
|
|
30
|
+
READ_TIMEOUT = 2.0
|
|
31
|
+
GOAL_CHARS = 140
|
|
32
|
+
NOT_TAKEN = "No action was taken."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# -- reading the screen ------------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def _clean(text):
|
|
38
|
+
text = (text or "").replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"')
|
|
39
|
+
return re.sub(r"\s+", " ", text).strip().casefold().strip(" .!?:")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# A security check or a block, in the apps' own words (English UI). Each pattern has to match one whole label (a title
|
|
43
|
+
# or a button, never a row or a field's value), so a message or a post that merely contains the words is
|
|
44
|
+
# not a challenge.
|
|
45
|
+
CHALLENGE = tuple(re.compile(pattern) for pattern in (
|
|
46
|
+
r"we suspect automated behavior( on your account)?",
|
|
47
|
+
r"suspicious (login|activity)( attempt| detected)?",
|
|
48
|
+
r"we detected unusual (login )?activity( on your account)?",
|
|
49
|
+
r"(please )?confirm it'?s you",
|
|
50
|
+
r"verify it'?s you",
|
|
51
|
+
r"help us confirm (you own this account|it'?s you)",
|
|
52
|
+
r"confirm your identity",
|
|
53
|
+
r"verify your identity",
|
|
54
|
+
r"(take|record|upload) a (video )?selfie",
|
|
55
|
+
r"upload a photo of your (id|government id)",
|
|
56
|
+
r"captcha",
|
|
57
|
+
r"(i'?m|i am) not a robot",
|
|
58
|
+
r"(confirm|prove|verify) (that )?you'?re (a )?(human|not a robot)",
|
|
59
|
+
r"verify you are (a )?human",
|
|
60
|
+
r"select all (the )?(images|squares|pictures) (with|containing) .{1,40}",
|
|
61
|
+
r"your account (has been|is|was) (temporarily )?(locked|suspended|disabled|restricted)",
|
|
62
|
+
r"we (have )?(temporarily )?(locked|restricted) your account",
|
|
63
|
+
))
|
|
64
|
+
# One of these alone says the session ended.
|
|
65
|
+
SIGNED_OUT_STRONG = tuple(re.compile(pattern) for pattern in (
|
|
66
|
+
r"you'?ve been logged out",
|
|
67
|
+
r"you have been logged out",
|
|
68
|
+
r"your session (has )?expired",
|
|
69
|
+
r"session expired",
|
|
70
|
+
r"(please )?(log|sign) in again( to .{1,40})?",
|
|
71
|
+
))
|
|
72
|
+
# A sign-in page shows at least two of these together (a feed never does).
|
|
73
|
+
SIGN_IN_PARTS = (
|
|
74
|
+
re.compile(r"(log ?in|sign in)( to .{1,30})?"),
|
|
75
|
+
re.compile(r"forgot (your )?password"),
|
|
76
|
+
re.compile(r"create (new |a )?account|sign up|get started"),
|
|
77
|
+
re.compile(r"(phone number|mobile number|username|email)(,| or ).{0,40}"),
|
|
78
|
+
re.compile(r"log into another account|use another profile|switch accounts"),
|
|
79
|
+
)
|
|
80
|
+
# What each kind of element can say. A title says what the screen is; a button is what the person can press; a field's
|
|
81
|
+
# label is its prompt ("Phone number, username or email address"). Cells and text views hold content (a post, a message,
|
|
82
|
+
# a row), and no element's value is read: those can carry anything another person wrote.
|
|
83
|
+
TITLES = frozenset({"StaticText", "NavigationBar", "Other", "Link"})
|
|
84
|
+
PROMPTS = TITLES | {"Button", "TextField"}
|
|
85
|
+
MAX_LABEL = 100
|
|
86
|
+
MAX_LABELS = 400
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _whole(patterns, text):
|
|
90
|
+
return any(pattern.fullmatch(text) for pattern in patterns)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def classify_screen(labels):
|
|
94
|
+
# screen text is data: only whole, short titles and buttons match, and nothing read is passed on as an instruction
|
|
95
|
+
pairs = []
|
|
96
|
+
for pair in list(labels or ())[:MAX_LABELS]:
|
|
97
|
+
if (isinstance(pair, (tuple, list)) and len(pair) == 2 and all(isinstance(part, str) for part in pair)
|
|
98
|
+
and len(pair[1]) <= 3 * MAX_LABEL):
|
|
99
|
+
text = _clean(pair[1])
|
|
100
|
+
if text and len(text) <= MAX_LABEL:
|
|
101
|
+
pairs.append((pair[0], text))
|
|
102
|
+
buttons = [text for role, text in pairs if role == "Button"]
|
|
103
|
+
titles = [text for role, text in pairs if role in TITLES]
|
|
104
|
+
if buttons and any(_whole(CHALLENGE, text) for text in titles):
|
|
105
|
+
return "challenge"
|
|
106
|
+
if any(_whole(SIGNED_OUT_STRONG, text) for role, text in pairs if role in TITLES or role == "Button"):
|
|
107
|
+
return "signed_out"
|
|
108
|
+
seen = {index for role, text in pairs if role in PROMPTS
|
|
109
|
+
for index, part in enumerate(SIGN_IN_PARTS) if part.fullmatch(text)}
|
|
110
|
+
return "signed_out" if len(seen) >= 2 else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def snapshot_labels(snapshot):
|
|
114
|
+
out = []
|
|
115
|
+
for element in list(getattr(snapshot, "elements", None) or ())[:MAX_LABELS]:
|
|
116
|
+
role, label = getattr(element, "role", None), getattr(element, "label", None)
|
|
117
|
+
if isinstance(role, str) and isinstance(label, str) and label:
|
|
118
|
+
out.append((role, label))
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# -- region, language and time zone ------------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
REGION = re.compile(r"[A-Z]{2}")
|
|
125
|
+
SETTING = {"region": "Settings › General › Language & Region › Region",
|
|
126
|
+
"language": "Settings › General › Language & Region › Preferred Languages",
|
|
127
|
+
"timezone": "Settings › General › Date & Time"}
|
|
128
|
+
WORD = {"region": "region", "language": "language", "timezone": "time zone"}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def expected_of(profile):
|
|
132
|
+
"""The values a profile expects: ``{region, language, timezone}``, each a string or None."""
|
|
133
|
+
out = {}
|
|
134
|
+
for key in ("region", "language", "timezone"):
|
|
135
|
+
value = (profile or {}).get(key)
|
|
136
|
+
out[key] = value if isinstance(value, str) and value else None
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@functools.lru_cache(maxsize=1)
|
|
141
|
+
def _iana_names():
|
|
142
|
+
try:
|
|
143
|
+
from zoneinfo import available_timezones
|
|
144
|
+
return frozenset(available_timezones())
|
|
145
|
+
except Exception: # noqa: BLE001 -- no tz data on this Mac: no zone can be told from an abbreviation
|
|
146
|
+
return frozenset()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def iana_zone(name):
|
|
150
|
+
# an abbreviation such as CST names no zone, so it is never compared with the zone a profile expects
|
|
151
|
+
if not isinstance(name, str):
|
|
152
|
+
return None
|
|
153
|
+
name = name.strip()
|
|
154
|
+
return name if (("/" in name or name in ("UTC", "GMT")) and name in _iana_names()) else None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_locale(info):
|
|
158
|
+
# /wda/device/info answers for the runner: its locale has the phone's Region but the runner's own language
|
|
159
|
+
info = info if isinstance(info, dict) else {}
|
|
160
|
+
region = None
|
|
161
|
+
identifier = info.get("currentLocale")
|
|
162
|
+
if isinstance(identifier, str):
|
|
163
|
+
parts = re.split(r"[_-]", identifier.split("@")[0].split(".")[0].strip())
|
|
164
|
+
found = [part for part in parts[1:] if REGION.fullmatch(part.upper()) and len(part) == 2]
|
|
165
|
+
region = found[-1].upper() if found else None
|
|
166
|
+
return {"region": region, "language": None, "timezone": iana_zone(info.get("timeZone"))}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _offsets(name):
|
|
170
|
+
"""The UTC offsets of an IANA zone on the first of four months this year, or None for a name that isn't one."""
|
|
171
|
+
try:
|
|
172
|
+
from zoneinfo import ZoneInfo
|
|
173
|
+
zone = ZoneInfo(name)
|
|
174
|
+
except Exception: # noqa: BLE001 -- an unknown name or no tz data: compared by name only
|
|
175
|
+
return None
|
|
176
|
+
year = datetime.now(timezone.utc).year
|
|
177
|
+
return tuple(datetime(year, month, 1, 12, tzinfo=zone).utcoffset() for month in (1, 4, 7, 10))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def zones_match(expected, actual):
|
|
181
|
+
if expected == actual:
|
|
182
|
+
return True
|
|
183
|
+
left, right = _offsets(expected), _offsets(actual)
|
|
184
|
+
return left is not None and left == right
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compare(profile, locale):
|
|
188
|
+
wanted = expected_of(profile)
|
|
189
|
+
locale = locale or {}
|
|
190
|
+
checks = []
|
|
191
|
+
for key in ("region", "language", "timezone"):
|
|
192
|
+
if wanted[key] is None:
|
|
193
|
+
continue
|
|
194
|
+
actual = locale.get(key)
|
|
195
|
+
if actual is None:
|
|
196
|
+
state = "unknown"
|
|
197
|
+
elif key == "timezone":
|
|
198
|
+
state = "match" if zones_match(wanted[key], actual) else "mismatch"
|
|
199
|
+
else:
|
|
200
|
+
state = "match" if wanted[key].casefold() == actual.casefold() else "mismatch"
|
|
201
|
+
checks.append({"field": key, "expected": wanted[key], "actual": actual, "state": state})
|
|
202
|
+
return checks
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def region_state(checks):
|
|
206
|
+
if any(check["state"] == "mismatch" for check in checks):
|
|
207
|
+
return "mismatch"
|
|
208
|
+
if any(check["state"] == "unknown" for check in checks):
|
|
209
|
+
return "unknown"
|
|
210
|
+
return "ok" if checks else "none"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def mismatch_message(profile_name, phone_name, checks):
|
|
214
|
+
"""The refusal: what differs (both values), what to change, and that nothing was done."""
|
|
215
|
+
bad = [check for check in checks if check["state"] == "mismatch"]
|
|
216
|
+
parts = [f"{WORD[c['field']]} {c['actual']} where {c['expected']} is expected" for c in bad]
|
|
217
|
+
changes = " and ".join(dict.fromkeys(SETTING[c["field"]] for c in bad))
|
|
218
|
+
return (f"{phone_name} doesn't match {profile_name}: it is set to {_join(parts)}. Change {changes} on the phone, "
|
|
219
|
+
f"or change what the profile expects in Settings › Profiles, then run the task again. {NOT_TAKEN}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _join(parts):
|
|
223
|
+
return parts[0] if len(parts) == 1 else ", ".join(parts[:-1]) + " and " + parts[-1]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def read_locale(wda_url, *, timeout=READ_TIMEOUT):
|
|
227
|
+
from . import native_helpers
|
|
228
|
+
if not wda_url or not native_helpers.devices_allowed():
|
|
229
|
+
return None
|
|
230
|
+
from .transport import HTTP
|
|
231
|
+
client = None
|
|
232
|
+
try:
|
|
233
|
+
client = HTTP(wda_url)
|
|
234
|
+
info = client.request("GET", "/wda/device/info", timeout=timeout).get("value")
|
|
235
|
+
except Exception: # noqa: BLE001 -- unknown, not blocking
|
|
236
|
+
return None
|
|
237
|
+
finally:
|
|
238
|
+
if client is not None:
|
|
239
|
+
client.close()
|
|
240
|
+
locale = parse_locale(info)
|
|
241
|
+
return locale if any(locale.values()) else None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# -- the alerts of one phone -------------------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def _alert(code, message, action, since=None, profile=None):
|
|
247
|
+
return {"code": code, "severity": SEVERITY[code], "message": message, "action": action, "since": since,
|
|
248
|
+
**({"profile": profile} if profile else {})}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def state_alerts(item, name):
|
|
252
|
+
"""The alerts the device list's own state gives: unplugged, locked, owner active."""
|
|
253
|
+
alerts = []
|
|
254
|
+
state, reason = item.get("state"), item.get("reason")
|
|
255
|
+
if state == "disconnected" and item.get("kind") == "usb" and not item.get("disconnected"):
|
|
256
|
+
away = reason == REASONS["wifi_unreachable"]
|
|
257
|
+
alerts.append(_alert("unplugged", f"{name} isn't reachable over Wi-Fi." if away else f"{name} is unplugged.",
|
|
258
|
+
"Keep it unlocked and on the same Wi-Fi as this Mac, or plug it in." if away
|
|
259
|
+
else "Plug it into this Mac with a cable."))
|
|
260
|
+
elif state == "connected" and reason == REASONS["locked"]:
|
|
261
|
+
alerts.append(_alert("locked", f"{name} is locked.", "Unlock it to run tasks. Mobster never unlocks a phone."))
|
|
262
|
+
owner = item.get("owner")
|
|
263
|
+
if isinstance(owner, dict) and owner.get("state") == "owner_active":
|
|
264
|
+
alerts.append(_alert("owner_active", f"You're using {name}.", "Mobster waits until you're done with it.",
|
|
265
|
+
since=owner.get("since") if isinstance(owner.get("since"), (int, float)) else None))
|
|
266
|
+
return alerts
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _app_name(profile):
|
|
270
|
+
app = (profile or {}).get("app")
|
|
271
|
+
name = app.get("name") if isinstance(app, dict) else None
|
|
272
|
+
return name if isinstance(name, str) and name else "the app"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _profile_name(profile):
|
|
276
|
+
app = (profile or {}).get("app")
|
|
277
|
+
handle = (profile or {}).get("handle") or "profile"
|
|
278
|
+
return f"@{handle} on {_app_name(profile)}" if isinstance(app, dict) and app.get("name") else f"@{handle}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def signal_alert(signal, profile_name, app_name, phone_name, since, profile_id):
|
|
282
|
+
if signal == "challenge":
|
|
283
|
+
return _alert("challenge", f"{app_name} asked for a security check for {profile_name} on {phone_name}.",
|
|
284
|
+
"Finish the check on the phone yourself, then choose Mark as handled. Tasks for this profile "
|
|
285
|
+
"wait until you do.", since, profile_id)
|
|
286
|
+
return _alert("signed_out", f"{profile_name} is signed out on {phone_name}.",
|
|
287
|
+
f"Open {app_name} on the phone and sign in yourself. Mobster never signs in for you.", since,
|
|
288
|
+
profile_id)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def region_alert(profile_name, phone_name, checks, since, profile_id):
|
|
292
|
+
bad = [check for check in checks if check["state"] == "mismatch"]
|
|
293
|
+
parts = [f"{WORD[c['field']]} {c['actual']} instead of {c['expected']}" for c in bad]
|
|
294
|
+
changes = " and ".join(dict.fromkeys(SETTING[c["field"]] for c in bad))
|
|
295
|
+
return _alert("region_mismatch", f"{phone_name} is set to {_join(parts)}, which {profile_name} doesn't expect.",
|
|
296
|
+
f"Change {changes} on the phone, or what the profile expects in Settings › Profiles.", since,
|
|
297
|
+
profile_id)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# -- persisted signals -------------------------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
LOCK_TIMEOUT = 5.0
|
|
303
|
+
BLANK = {"signal": None, "since": None, "device": None, "confirmedAt": None}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class Signals:
|
|
307
|
+
"""fleet-health.json: per profile id, ``{signal, since, device, confirmedAt}``."""
|
|
308
|
+
|
|
309
|
+
def __init__(self, folder, clock=time.time):
|
|
310
|
+
self.path = Path(folder) / STORE if folder is not None else None
|
|
311
|
+
self.lock_path = self.path.with_suffix(".lock") if self.path is not None else None
|
|
312
|
+
self.clock = clock
|
|
313
|
+
self.lock = threading.Lock() # the in-memory copy, used only without a folder
|
|
314
|
+
self.memory = {}
|
|
315
|
+
|
|
316
|
+
def _load(self):
|
|
317
|
+
if self.path is None:
|
|
318
|
+
return {key: dict(entry) for key, entry in self.memory.items()}
|
|
319
|
+
try:
|
|
320
|
+
raw = json.loads(self.path.read_text())
|
|
321
|
+
except (OSError, ValueError):
|
|
322
|
+
return {}
|
|
323
|
+
profiles = raw.get("profiles") if isinstance(raw, dict) else None
|
|
324
|
+
out = {}
|
|
325
|
+
for key, entry in (profiles.items() if isinstance(profiles, dict) else ()):
|
|
326
|
+
if isinstance(key, str) and isinstance(entry, dict):
|
|
327
|
+
signal = entry.get("signal") if entry.get("signal") in ("challenge", "signed_out") else None
|
|
328
|
+
out[key] = {"signal": signal,
|
|
329
|
+
"since": entry.get("since") if isinstance(entry.get("since"), int) else None,
|
|
330
|
+
"device": entry.get("device") if isinstance(entry.get("device"), str) else None,
|
|
331
|
+
"confirmedAt": (entry.get("confirmedAt") if isinstance(entry.get("confirmedAt"), int)
|
|
332
|
+
else None)}
|
|
333
|
+
return out
|
|
334
|
+
|
|
335
|
+
def _save(self, data):
|
|
336
|
+
if self.path is None:
|
|
337
|
+
self.memory = data
|
|
338
|
+
return
|
|
339
|
+
fd, temporary = tempfile.mkstemp(prefix=".fleet-health.", suffix=".json", dir=self.path.parent)
|
|
340
|
+
try:
|
|
341
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
342
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
343
|
+
stream.write(json.dumps({"version": 1, "profiles": data}, indent=1) + "\n")
|
|
344
|
+
os.replace(temporary, self.path)
|
|
345
|
+
except BaseException:
|
|
346
|
+
with contextlib.suppress(OSError):
|
|
347
|
+
os.unlink(temporary)
|
|
348
|
+
raise
|
|
349
|
+
|
|
350
|
+
@contextlib.contextmanager
|
|
351
|
+
def _edit(self):
|
|
352
|
+
# the Mac app's server and `mobster mcp` both write: every edit locks, reads again and writes atomically
|
|
353
|
+
if self.path is None:
|
|
354
|
+
with self.lock:
|
|
355
|
+
data = self._load()
|
|
356
|
+
before = json.dumps(data, sort_keys=True)
|
|
357
|
+
yield data
|
|
358
|
+
if json.dumps(data, sort_keys=True) != before:
|
|
359
|
+
self._save(data)
|
|
360
|
+
return
|
|
361
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
362
|
+
fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
363
|
+
try:
|
|
364
|
+
end = time.monotonic() + LOCK_TIMEOUT
|
|
365
|
+
while True:
|
|
366
|
+
try:
|
|
367
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
368
|
+
break
|
|
369
|
+
except OSError as exc:
|
|
370
|
+
if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or time.monotonic() >= end:
|
|
371
|
+
raise OSError("Another Mobster process is holding the phone health file") from None
|
|
372
|
+
time.sleep(.02)
|
|
373
|
+
data = self._load()
|
|
374
|
+
before = json.dumps(data, sort_keys=True)
|
|
375
|
+
yield data
|
|
376
|
+
if json.dumps(data, sort_keys=True) != before:
|
|
377
|
+
self._save(data)
|
|
378
|
+
finally:
|
|
379
|
+
os.close(fd)
|
|
380
|
+
|
|
381
|
+
def all(self):
|
|
382
|
+
"""Every profile's entry, read from the file now."""
|
|
383
|
+
if self.path is None:
|
|
384
|
+
with self.lock:
|
|
385
|
+
return self._load()
|
|
386
|
+
return self._load()
|
|
387
|
+
|
|
388
|
+
def get(self, profile_id):
|
|
389
|
+
return dict(self.all().get(profile_id) or {})
|
|
390
|
+
|
|
391
|
+
def mark(self, profile_id, signal, device):
|
|
392
|
+
"""Record ``signal``; a challenge is never replaced by the milder signed_out."""
|
|
393
|
+
now = round(self.clock() * 1000)
|
|
394
|
+
with self._edit() as data:
|
|
395
|
+
entry = data.setdefault(profile_id, dict(BLANK))
|
|
396
|
+
if entry["signal"] == "challenge" and signal == "signed_out":
|
|
397
|
+
return
|
|
398
|
+
if entry["signal"] != signal:
|
|
399
|
+
entry.update(signal=signal, since=now)
|
|
400
|
+
entry["device"] = device
|
|
401
|
+
|
|
402
|
+
def confirm(self, profile_id, device, clear=True):
|
|
403
|
+
now = round(self.clock() * 1000)
|
|
404
|
+
with self._edit() as data:
|
|
405
|
+
entry = data.setdefault(profile_id, dict(BLANK))
|
|
406
|
+
entry["confirmedAt"], entry["device"] = now, device
|
|
407
|
+
if clear and entry["signal"] != "challenge":
|
|
408
|
+
entry["signal"], entry["since"] = None, None
|
|
409
|
+
|
|
410
|
+
def clear(self, profile_id, signal=None):
|
|
411
|
+
with self._edit() as data:
|
|
412
|
+
entry = data.get(profile_id)
|
|
413
|
+
if not entry or not entry["signal"] or (signal and entry["signal"] != signal):
|
|
414
|
+
return False
|
|
415
|
+
entry["signal"], entry["since"] = None, None
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
def forget(self, profile_id):
|
|
419
|
+
with self._edit() as data:
|
|
420
|
+
data.pop(profile_id, None)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# -- the service object -------------------------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
def _goal(text):
|
|
426
|
+
text = " ".join(str(text or "").split())
|
|
427
|
+
return text if len(text) <= GOAL_CHARS else text[:GOAL_CHARS - 1].rstrip() + "…"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class FleetHealth:
|
|
431
|
+
"""Computes ``health`` for the fleet's phones and fires the edge-triggered alerts. One per Fleet."""
|
|
432
|
+
|
|
433
|
+
def __init__(self, fleet, clock=time.time, monotonic=time.monotonic, read=read_locale, alerts=None, publish=None,
|
|
434
|
+
background=True, debounce=DEBOUNCE_S):
|
|
435
|
+
self.fleet = fleet
|
|
436
|
+
self.clock, self.monotonic, self.read = clock, monotonic, read
|
|
437
|
+
self._alerts, self._publish, self.background, self.debounce = alerts, publish, background, debounce
|
|
438
|
+
self.signals = Signals(getattr(fleet, "data_dir", None), clock)
|
|
439
|
+
self.lock = threading.Lock()
|
|
440
|
+
self.locales = {} # phone id -> (monotonic time read, locale or None)
|
|
441
|
+
self.reading = set() # phone ids being read now
|
|
442
|
+
self.active = {} # (phone id, code, profile id) -> {first, sent, gone}
|
|
443
|
+
self.baselined = False
|
|
444
|
+
self.sent = [] # (phone id, code, profile id) of every alert delivered, for tests and the log
|
|
445
|
+
|
|
446
|
+
# -- inputs --------------------------------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def runtime(self):
|
|
450
|
+
return self.fleet.runtime
|
|
451
|
+
|
|
452
|
+
def profiles(self):
|
|
453
|
+
try:
|
|
454
|
+
from .accounts.store import store_for
|
|
455
|
+
return store_for(self.runtime).all()
|
|
456
|
+
except Exception: # noqa: BLE001 -- Profiles off: no profile checks
|
|
457
|
+
return []
|
|
458
|
+
|
|
459
|
+
def profiles_on(self, phone, item, profiles=None):
|
|
460
|
+
profiles = self.profiles() if profiles is None else profiles
|
|
461
|
+
return [p for p in profiles if p.get("device") == phone.id or (not p.get("device") and item.get("default"))]
|
|
462
|
+
|
|
463
|
+
def locale(self, phone, ttl=LOCALE_TTL, read_now=False):
|
|
464
|
+
"""The phone's cached locale, or None. ``read_now`` reads again when the cache is older than ``ttl``."""
|
|
465
|
+
with self.lock:
|
|
466
|
+
cached = self.locales.get(phone.id)
|
|
467
|
+
fresh = cached is not None and self.monotonic() - cached[0] < ttl
|
|
468
|
+
if fresh or not read_now:
|
|
469
|
+
return cached[1] if cached else None
|
|
470
|
+
locale = self.read(phone.wda_url)
|
|
471
|
+
with self.lock:
|
|
472
|
+
self.locales[phone.id] = (self.monotonic(), locale)
|
|
473
|
+
return locale
|
|
474
|
+
|
|
475
|
+
def readable(self, phone):
|
|
476
|
+
try:
|
|
477
|
+
disconnected = getattr(self.fleet, "is_disconnected", None)
|
|
478
|
+
if callable(disconnected) and disconnected(phone):
|
|
479
|
+
return False
|
|
480
|
+
in_use = getattr(self.fleet, "in_use", None)
|
|
481
|
+
if callable(in_use):
|
|
482
|
+
return not in_use(phone)
|
|
483
|
+
busy = getattr(self.fleet, "busy", None)
|
|
484
|
+
return not (callable(busy) and busy(phone) is not None)
|
|
485
|
+
except Exception: # noqa: BLE001 -- fail closed: nothing is read from a phone whose state can't be told
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
def _refresh(self, phone, item, wanted):
|
|
489
|
+
"""Read an idle ready phone's locale in the background when a profile on it expects one and the cache is old."""
|
|
490
|
+
if (not wanted or item.get("state") != "ready" or item.get("activeRunId") or item.get("disconnected")
|
|
491
|
+
or not phone.wda_url):
|
|
492
|
+
return
|
|
493
|
+
with self.lock:
|
|
494
|
+
cached = self.locales.get(phone.id)
|
|
495
|
+
if phone.id in self.reading or (cached is not None and self.monotonic() - cached[0] < LOCALE_TTL):
|
|
496
|
+
return
|
|
497
|
+
self.reading.add(phone.id)
|
|
498
|
+
if not self.readable(phone):
|
|
499
|
+
with self.lock:
|
|
500
|
+
self.reading.discard(phone.id)
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
def work():
|
|
504
|
+
try:
|
|
505
|
+
self.locale(phone, ttl=0, read_now=True)
|
|
506
|
+
finally:
|
|
507
|
+
with self.lock:
|
|
508
|
+
self.reading.discard(phone.id)
|
|
509
|
+
if self.background:
|
|
510
|
+
_Thread(target=work, name="mobster-locale", daemon=True).start()
|
|
511
|
+
else:
|
|
512
|
+
work()
|
|
513
|
+
|
|
514
|
+
def last_good(self, phone):
|
|
515
|
+
runs = getattr(self.runtime, "runs", None) or {}
|
|
516
|
+
best = None
|
|
517
|
+
for run in list(runs.values()):
|
|
518
|
+
if getattr(run, "status", None) != "completed" or getattr(run, "finished_at", None) is None:
|
|
519
|
+
continue
|
|
520
|
+
if getattr(run, "mode", "live") != "live":
|
|
521
|
+
continue
|
|
522
|
+
device = getattr(run, "device_id", None)
|
|
523
|
+
if device != phone.id and not (device is None and phone.primary):
|
|
524
|
+
continue
|
|
525
|
+
if best is None or run.finished_at > best.finished_at:
|
|
526
|
+
best = run
|
|
527
|
+
if best is None:
|
|
528
|
+
return None
|
|
529
|
+
return {"runId": best.id, "at": round(best.finished_at * 1000), "summary": _goal(best.goal)}
|
|
530
|
+
|
|
531
|
+
def current_task(self, item):
|
|
532
|
+
run_id = item.get("activeRunId")
|
|
533
|
+
run = (getattr(self.runtime, "runs", None) or {}).get(run_id) if run_id else None
|
|
534
|
+
if run is not None:
|
|
535
|
+
return {"runId": run_id, "goal": _goal(run.goal), "since": round(run.created_at * 1000)}
|
|
536
|
+
holder = item.get("holder")
|
|
537
|
+
if isinstance(holder, dict) and isinstance(holder.get("label"), str):
|
|
538
|
+
since = holder.get("since")
|
|
539
|
+
return {"runId": holder.get("runId") if isinstance(holder.get("runId"), str) else None,
|
|
540
|
+
"goal": _goal(holder["label"]), "kind": holder.get("kind") if isinstance(holder.get("kind"), str)
|
|
541
|
+
else None, "since": since if isinstance(since, (int, float)) else None}
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
# -- one phone's health --------------------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
def health_of(self, phone, item, profiles=None, signals=None):
|
|
547
|
+
"""``health`` for one phone's record. Reads nothing from the phone: its locale comes from the cache."""
|
|
548
|
+
signals = self.signals.all() if signals is None else signals
|
|
549
|
+
name = item.get("name") or "This iPhone"
|
|
550
|
+
mine = self.profiles_on(phone, item, profiles)
|
|
551
|
+
wanted = any(any(expected_of(p).values()) for p in mine)
|
|
552
|
+
self._refresh(phone, item, wanted)
|
|
553
|
+
locale = self.locale(phone)
|
|
554
|
+
alerts = state_alerts(item, name)
|
|
555
|
+
report, rows = [], []
|
|
556
|
+
for profile in mine:
|
|
557
|
+
label = _profile_name(profile)
|
|
558
|
+
stored = signals.get(profile["id"]) or {}
|
|
559
|
+
signal = stored.get("signal")
|
|
560
|
+
checks = compare(profile, locale)
|
|
561
|
+
report.append({"profile": profile["id"], "name": label, "state": region_state(checks), "checks": checks})
|
|
562
|
+
if signal:
|
|
563
|
+
alerts.append(signal_alert(signal, label, _app_name(profile), name, stored.get("since"),
|
|
564
|
+
profile["id"]))
|
|
565
|
+
if any(check["state"] == "mismatch" for check in checks):
|
|
566
|
+
alerts.append(region_alert(label, name, checks, None, profile["id"]))
|
|
567
|
+
expected = expected_of(profile)
|
|
568
|
+
rows.append({"id": profile["id"], "name": label, "handle": profile.get("handle"), "app": _app_name(profile),
|
|
569
|
+
"signedIn": False if signal else (True if stored.get("confirmedAt") else None), **expected})
|
|
570
|
+
overall = "none"
|
|
571
|
+
for entry in report:
|
|
572
|
+
if entry["state"] == "mismatch":
|
|
573
|
+
overall = "mismatch"
|
|
574
|
+
elif entry["state"] == "unknown" and overall != "mismatch":
|
|
575
|
+
overall = "unknown"
|
|
576
|
+
elif entry["state"] == "ok" and overall == "none":
|
|
577
|
+
overall = "ok"
|
|
578
|
+
region = {"state": overall, "profiles": report, "phone": {**{k: (locale or {}).get(k) for k in
|
|
579
|
+
("region", "language", "timezone")}, "readAt": self._read_at(phone)}}
|
|
580
|
+
return {"alerts": alerts, "lastGoodRun": self.last_good(phone), "profiles": rows, "region": region,
|
|
581
|
+
"currentTask": self.current_task(item)}
|
|
582
|
+
|
|
583
|
+
def _read_at(self, phone):
|
|
584
|
+
with self.lock:
|
|
585
|
+
cached = self.locales.get(phone.id)
|
|
586
|
+
if cached is None or cached[1] is None:
|
|
587
|
+
return None
|
|
588
|
+
return round((self.clock() - (self.monotonic() - cached[0])) * 1000)
|
|
589
|
+
|
|
590
|
+
def apply(self, rows, complete=True):
|
|
591
|
+
profiles, signals = self.profiles(), self.signals.all()
|
|
592
|
+
current = {}
|
|
593
|
+
for phone, item in rows:
|
|
594
|
+
if not phone.id:
|
|
595
|
+
continue
|
|
596
|
+
health = self.health_of(phone, item, profiles, signals)
|
|
597
|
+
for alert in health["alerts"]:
|
|
598
|
+
current[(phone.id, alert["code"], alert.get("profile"))] = (item, alert)
|
|
599
|
+
item["health"] = health
|
|
600
|
+
self.observe(current, None if complete else {phone.id for phone, _ in rows if phone.id})
|
|
601
|
+
# The alerts' ``since``: when each condition was first seen, unless the source knew better.
|
|
602
|
+
for phone, item in rows:
|
|
603
|
+
for alert in (item.get("health") or {}).get("alerts", ()):
|
|
604
|
+
if alert["since"] is None:
|
|
605
|
+
seen = self.active.get((phone.id, alert["code"], alert.get("profile")))
|
|
606
|
+
alert["since"] = round(seen["first_wall"] * 1000) if seen else round(self.clock() * 1000)
|
|
607
|
+
|
|
608
|
+
# -- edge-triggered alerts -----------------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
def observe(self, current, phones=None):
|
|
611
|
+
now, wall = self.monotonic(), self.clock()
|
|
612
|
+
delivered = []
|
|
613
|
+
with self.lock:
|
|
614
|
+
first_pass = not self.baselined
|
|
615
|
+
if first_pass:
|
|
616
|
+
if phones is not None: # the baseline is a read of every phone: one phone's can't tell what held
|
|
617
|
+
return []
|
|
618
|
+
self.baselined = True
|
|
619
|
+
for key, (item, alert) in current.items():
|
|
620
|
+
entry = self.active.get(key)
|
|
621
|
+
if entry is None:
|
|
622
|
+
entry = self.active[key] = {"first": now, "first_wall": wall, "sent": first_pass, "gone": None}
|
|
623
|
+
elif entry["gone"] is not None and not entry["sent"]:
|
|
624
|
+
entry["first"], entry["first_wall"] = now, wall
|
|
625
|
+
entry["gone"] = None
|
|
626
|
+
if not entry["sent"] and now - entry["first"] >= self.debounce:
|
|
627
|
+
entry["sent"] = True
|
|
628
|
+
if alert["severity"] == ATTENTION:
|
|
629
|
+
delivered.append((key, item, alert))
|
|
630
|
+
for key in [k for k in self.active if k not in current and (phones is None or k[0] in phones)]:
|
|
631
|
+
entry = self.active[key]
|
|
632
|
+
if entry["gone"] is None:
|
|
633
|
+
entry["gone"] = now
|
|
634
|
+
elif now - entry["gone"] >= self.debounce:
|
|
635
|
+
del self.active[key]
|
|
636
|
+
for key, item, alert in delivered:
|
|
637
|
+
self._deliver(key, item, alert)
|
|
638
|
+
return [key for key, _, _ in delivered]
|
|
639
|
+
|
|
640
|
+
def _deliver(self, key, item, alert):
|
|
641
|
+
self.sent.append(key)
|
|
642
|
+
name = item.get("name") or "iPhone"
|
|
643
|
+
body = f"{alert['message']} {alert['action']}"
|
|
644
|
+
event = {"event": "phone_alert", "device": key[0], "deviceName": name, "code": alert["code"],
|
|
645
|
+
"profile": key[2], "title": f"Mobster: {name}", "body": body}
|
|
646
|
+
with contextlib.suppress(Exception):
|
|
647
|
+
if self._publish is not None:
|
|
648
|
+
self._publish("devices", event)
|
|
649
|
+
else:
|
|
650
|
+
self.runtime.publish("devices", event)
|
|
651
|
+
with contextlib.suppress(Exception):
|
|
652
|
+
alerts = self._alerts
|
|
653
|
+
if alerts is None:
|
|
654
|
+
from .alerts import Alerts
|
|
655
|
+
alerts = self._alerts = Alerts()
|
|
656
|
+
alerts.phone_alert(name, alert["code"], alert["message"], alert["action"])
|
|
657
|
+
|
|
658
|
+
# -- the runs ------------------------------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
def note_screen(self, account, device, signal):
|
|
661
|
+
if signal in ("challenge", "signed_out") and isinstance(account, dict) and account.get("id"):
|
|
662
|
+
with contextlib.suppress(Exception):
|
|
663
|
+
self.signals.mark(account["id"], signal, device)
|
|
664
|
+
|
|
665
|
+
def note_run(self, run):
|
|
666
|
+
"""A finished run: keep what it learned about its profile. Never raises."""
|
|
667
|
+
try:
|
|
668
|
+
self._note_run(run)
|
|
669
|
+
except Exception: # noqa: BLE001 -- health is never part of a run's outcome
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
def _note_run(self, run):
|
|
673
|
+
account = (getattr(run, "extras", None) or {}).get("account")
|
|
674
|
+
if not isinstance(account, dict) or not account.get("id"):
|
|
675
|
+
return
|
|
676
|
+
device = getattr(run, "device_id", None)
|
|
677
|
+
summary = run.summary if isinstance(run.summary, dict) else {}
|
|
678
|
+
latest = None
|
|
679
|
+
for event in list(run.events):
|
|
680
|
+
if event.get("event") == "account_signal" and event.get("account") == account["id"]:
|
|
681
|
+
if event.get("signal") in ("challenge", "signed_out"):
|
|
682
|
+
latest = "challenge" if event["signal"] == "challenge" or latest == "challenge" else "signed_out"
|
|
683
|
+
elif (event.get("event") == "account_checked" and event.get("account") == account["id"]
|
|
684
|
+
and event.get("state") == "confirmed" and latest != "challenge"):
|
|
685
|
+
latest = "confirmed"
|
|
686
|
+
if (summary.get("stop_code") or summary.get("code")) in ("account_unconfirmed", "account_mismatch") \
|
|
687
|
+
and latest not in ("challenge", "signed_out"):
|
|
688
|
+
latest = "signed_out"
|
|
689
|
+
if latest in ("challenge", "signed_out"):
|
|
690
|
+
self.signals.mark(account["id"], latest, device)
|
|
691
|
+
elif latest == "confirmed":
|
|
692
|
+
self.signals.confirm(account["id"], device)
|
|
693
|
+
|
|
694
|
+
def acknowledge(self, profile_id, code=None):
|
|
695
|
+
"""The person says they handled a challenge or a sign-in. True when an alert was cleared."""
|
|
696
|
+
if code is not None and code not in ("challenge", "signed_out"):
|
|
697
|
+
raise ValueError("code is challenge or signed_out")
|
|
698
|
+
return self.signals.clear(profile_id, code)
|
|
699
|
+
|
|
700
|
+
# -- the pre-run check ---------------------------------------------------------------------------------------
|
|
701
|
+
|
|
702
|
+
def preflight(self, account, phone):
|
|
703
|
+
stored = next((p for p in self.profiles() if p["id"] == account.get("id")), None) or account
|
|
704
|
+
name = phone.name or "This iPhone"
|
|
705
|
+
label = _profile_name(stored)
|
|
706
|
+
signal = self.signals.get(stored["id"]).get("signal") if stored.get("id") else None
|
|
707
|
+
if signal == "challenge":
|
|
708
|
+
raise APIError(f"{_app_name(stored)} asked for a security check for {label}. Finish it on the phone "
|
|
709
|
+
f"yourself, then choose Mark as handled on the Phones page. {NOT_TAKEN}", 409,
|
|
710
|
+
"profile_challenge", device=phone.id, profile=stored["id"])
|
|
711
|
+
wanted = expected_of(stored)
|
|
712
|
+
if not any(wanted.values()):
|
|
713
|
+
return
|
|
714
|
+
if not self.readable(phone):
|
|
715
|
+
return # disconnected, or a task runs there: nothing is read, and the run waits or is refused on its own
|
|
716
|
+
checks = compare(stored, self.locale(phone, ttl=PRE_RUN_TTL, read_now=True))
|
|
717
|
+
if any(check["state"] == "mismatch" for check in checks):
|
|
718
|
+
raise APIError(mismatch_message(label, name, checks), 409, "profile_region_mismatch", device=phone.id,
|
|
719
|
+
profile=stored["id"], checks=checks)
|
|
720
|
+
|
|
721
|
+
# -- the API -------------------------------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
def report(self):
|
|
724
|
+
"""GET /api/fleet/health: ``{devices: [{id, name, state, health}], checkedAt}``, cheap (Fleet.entries)."""
|
|
725
|
+
rows = self.fleet.entries()
|
|
726
|
+
return {"devices": [{"id": item["id"], "name": item.get("name"), "state": item.get("state"),
|
|
727
|
+
"health": item.get("health")} for _, item in rows],
|
|
728
|
+
"checkedAt": round(self.clock() * 1000)}
|
|
729
|
+
|