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,271 @@
|
|
|
1
|
+
"""Bounded HTTP with one deadline per operation and no automatic retry: an interrupted request's outcome is unknown."""
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import select
|
|
7
|
+
import socket
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from urllib.parse import urlsplit
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .errors import MobsterError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def user_agent():
|
|
17
|
+
"""The shared client identity used when calling Mobster services."""
|
|
18
|
+
import platform
|
|
19
|
+
return f"Mobster/{__version__} (macOS {platform.mac_ver()[0] or 'unknown'})"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TransportError(MobsterError):
|
|
23
|
+
"""``status`` and ``wda_error``: a failed answer's HTTP status and WDA's error name, when there was an answer.
|
|
24
|
+
Not its message, which can hold app text."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, *args, status=None, wda_error=None):
|
|
27
|
+
super().__init__(*args)
|
|
28
|
+
self.status, self.wda_error = status, wda_error
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# The request never left, so its outcome is known: nothing happened.
|
|
32
|
+
class NotSent(TransportError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# WDA checks the session before it runs anything, so nothing ran.
|
|
37
|
+
class StaleSession(NotSent):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Every error name WebDriverAgent v16.12.10 answers with, and the one HTTP status it pairs with each
|
|
42
|
+
# (WebDriverAgentLib/Routing/FBCommandStatus.m). Anything else isn't WDA's name and is never kept: it could be app text.
|
|
43
|
+
WDA_ERRORS = {
|
|
44
|
+
"unknown error": 500, "unable to capture screen": 500, "no such element": 404, "invalid element state": 400,
|
|
45
|
+
"invalid argument": 400, "stale element reference": 404, "invalid selector": 400, "no such alert": 404,
|
|
46
|
+
"unexpected alert open": 500, "unknown method": 405, "session not created": 500, "invalid coordinates": 400,
|
|
47
|
+
"unsupported operation": 500, "unknown command": 404, "timeout": 408, "element not visible": 400,
|
|
48
|
+
"invalid session id": 404,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def error_name(value):
|
|
53
|
+
"""WDA's error name from an answer's ``value``, or None: only one of WDA_ERRORS' names, exactly."""
|
|
54
|
+
name = value.get("error") if isinstance(value, dict) else None
|
|
55
|
+
return name if isinstance(name, str) and name in WDA_ERRORS else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def answer_error(raw):
|
|
59
|
+
"""Strictly decoded: a body with two ``error`` fields, say, names nothing."""
|
|
60
|
+
try:
|
|
61
|
+
return error_name(decode_json(raw).get("value"))
|
|
62
|
+
except (ValueError, AttributeError, UnicodeDecodeError, RecursionError):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Deadline:
|
|
67
|
+
def __init__(self, timeout):
|
|
68
|
+
if type(timeout) not in (int, float) or not math.isfinite(timeout) or timeout <= 0:
|
|
69
|
+
raise ValueError("Timeout must be a finite positive number")
|
|
70
|
+
self.end = time.monotonic() + timeout
|
|
71
|
+
|
|
72
|
+
def remaining(self):
|
|
73
|
+
left = self.end - time.monotonic()
|
|
74
|
+
if left <= 0:
|
|
75
|
+
raise TimeoutError("Operation deadline exceeded")
|
|
76
|
+
return left
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _unique_object(pairs):
|
|
80
|
+
# dict(pairs) then a length check: 4-6% faster than a Python loop on a 66 KB WDA tree (~400 us), 2026-09-24.
|
|
81
|
+
result = dict(pairs)
|
|
82
|
+
if len(result) != len(pairs):
|
|
83
|
+
raise ValueError("Duplicate JSON field")
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _invalid_constant(value):
|
|
88
|
+
raise ValueError("Nonfinite JSON value")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _finite_float(value):
|
|
92
|
+
result = float(value)
|
|
93
|
+
if not math.isfinite(result):
|
|
94
|
+
raise ValueError("Nonfinite JSON value")
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def decode_json(raw):
|
|
99
|
+
return json.loads(raw, parse_constant=_invalid_constant, parse_float=_finite_float,
|
|
100
|
+
object_pairs_hook=_unique_object)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class HTTP:
|
|
104
|
+
def __init__(self, base_url, key="", timeout=20):
|
|
105
|
+
url = urlsplit(base_url)
|
|
106
|
+
if (url.scheme not in {"http", "https"} or not url.hostname or url.username or url.password
|
|
107
|
+
or url.query or url.fragment):
|
|
108
|
+
raise ValueError("Expected HTTP(S) base URL without embedded credentials")
|
|
109
|
+
if key and url.scheme != "https" and url.hostname not in {"localhost", "127.0.0.1", "::1"}:
|
|
110
|
+
raise ValueError("Remote credentials require HTTPS")
|
|
111
|
+
cls = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
|
|
112
|
+
self.connection = cls(url.hostname, url.port, timeout=timeout)
|
|
113
|
+
self.prefix, self.key = url.path.rstrip("/"), key
|
|
114
|
+
self._inflight = threading.Lock()
|
|
115
|
+
|
|
116
|
+
def request(self, method, path, body=None, timeout=20):
|
|
117
|
+
deadline = Deadline(timeout)
|
|
118
|
+
if not isinstance(path, str) or not path.startswith("/") or any(c in path for c in "\r\n"):
|
|
119
|
+
raise ValueError("Expected an absolute HTTP request path")
|
|
120
|
+
# Encoding/validation precedes dispatch; invalid input cannot poison a live connection.
|
|
121
|
+
payload = None if body is None else json.dumps(body, allow_nan=False).encode()
|
|
122
|
+
if not self._inflight.acquire(blocking=False):
|
|
123
|
+
raise TransportError("HTTP client already has a request in flight; not retried")
|
|
124
|
+
headers = {"Content-Type": "application/json"}
|
|
125
|
+
if self.key:
|
|
126
|
+
headers["Authorization"] = "Bearer " + self.key
|
|
127
|
+
guard, finished = threading.Lock(), threading.Event()
|
|
128
|
+
active_socket = {"value": None}
|
|
129
|
+
answered_in_full = False
|
|
130
|
+
|
|
131
|
+
def expire():
|
|
132
|
+
# Socket timeouts restart on each chunk: cut a slow drip at the absolute deadline.
|
|
133
|
+
with guard:
|
|
134
|
+
if finished.is_set():
|
|
135
|
+
return
|
|
136
|
+
sock = active_socket["value"] or self.connection.sock
|
|
137
|
+
if sock:
|
|
138
|
+
try:
|
|
139
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
self.connection.close()
|
|
143
|
+
|
|
144
|
+
timer = threading.Timer(max(0, deadline.end - time.monotonic()), expire)
|
|
145
|
+
timer.daemon = True
|
|
146
|
+
timer.start()
|
|
147
|
+
try:
|
|
148
|
+
if self.connection.sock is not None and peer_closed(self.connection.sock):
|
|
149
|
+
# A kept connection the server has closed since, as WDA's failClient does after a header-level 400
|
|
150
|
+
# with no Connection header. Nothing of this request is sent yet, so a new connection resends nothing.
|
|
151
|
+
self.connection.close()
|
|
152
|
+
self.connection.timeout = deadline.remaining()
|
|
153
|
+
if self.connection.sock:
|
|
154
|
+
self.connection.sock.settimeout(deadline.remaining())
|
|
155
|
+
self.connection.request(method, self.prefix + path, payload, headers)
|
|
156
|
+
active_socket["value"] = self.connection.sock
|
|
157
|
+
deadline.remaining()
|
|
158
|
+
response = self.connection.getresponse()
|
|
159
|
+
raw = response.read(16_000_001)
|
|
160
|
+
deadline.remaining()
|
|
161
|
+
if len(raw) > 16_000_000:
|
|
162
|
+
raise TransportError("HTTP response exceeds limit; outcome unknown; not retried")
|
|
163
|
+
if isinstance(getattr(response, "length", None), int) and response.length != 0:
|
|
164
|
+
raise TransportError("Incomplete HTTP response; outcome unknown; not retried")
|
|
165
|
+
if response.status >= 300:
|
|
166
|
+
# Read to its end, so the connection is ready for the next request. WebDriverAgent keeps a descriptor
|
|
167
|
+
# per connection until it restarts: routine 404s would each cost one.
|
|
168
|
+
answered_in_full = response.isclosed()
|
|
169
|
+
name = answer_error(raw)
|
|
170
|
+
if response.status == 404 and name == "invalid session id":
|
|
171
|
+
raise StaleSession("HTTP 404 StaleSession: invalid session id; nothing sent; not retried")
|
|
172
|
+
raise TransportError(f"HTTP {response.status}; request not retried",
|
|
173
|
+
status=response.status, wda_error=name)
|
|
174
|
+
result = decode_json(raw)
|
|
175
|
+
deadline.remaining()
|
|
176
|
+
if not isinstance(result, dict):
|
|
177
|
+
raise TransportError("HTTP response must be an object; outcome unknown; not retried")
|
|
178
|
+
return result
|
|
179
|
+
except TransportError:
|
|
180
|
+
if not answered_in_full:
|
|
181
|
+
self.connection.close()
|
|
182
|
+
raise
|
|
183
|
+
except (OSError, http.client.HTTPException, ValueError, RecursionError) as exc:
|
|
184
|
+
self.connection.close()
|
|
185
|
+
if isinstance(exc, ConnectionRefusedError):
|
|
186
|
+
raise NotSent(f"HTTP {type(exc).__name__}; nothing sent; not retried") from None
|
|
187
|
+
raise TransportError(f"HTTP {type(exc).__name__}; request outcome unknown; not retried") from None
|
|
188
|
+
finally:
|
|
189
|
+
with guard:
|
|
190
|
+
finished.set()
|
|
191
|
+
timer.cancel()
|
|
192
|
+
self._inflight.release()
|
|
193
|
+
|
|
194
|
+
def close(self):
|
|
195
|
+
self.connection.close()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def peer_closed(sock):
|
|
199
|
+
"""Whether an idle kept socket has anything to read, which on an idle HTTP connection means the server closed it
|
|
200
|
+
(or sent something unasked). Either way it can't carry the next request. A socket that can't be asked is closed."""
|
|
201
|
+
try:
|
|
202
|
+
if getattr(sock, "pending", lambda: 0)():
|
|
203
|
+
return True
|
|
204
|
+
# poll, not select: select refuses a descriptor numbered 1024 or more, which would read every socket as closed
|
|
205
|
+
# and bring back the per-request connections #2058 removed.
|
|
206
|
+
probe = select.poll()
|
|
207
|
+
probe.register(sock, select.POLLIN)
|
|
208
|
+
return bool(probe.poll(0))
|
|
209
|
+
except (OSError, ValueError):
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def request_once(base_url, method, path, body=None, timeout=20):
|
|
214
|
+
client = HTTP(base_url)
|
|
215
|
+
try:
|
|
216
|
+
return client.request(method, path, body, timeout=timeout)
|
|
217
|
+
finally:
|
|
218
|
+
client.close()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# A new connection per repeated read ran WebDriverAgent out of file descriptors in about 14 minutes (17 Oct).
|
|
222
|
+
KEPT_MAX = 16
|
|
223
|
+
CLOSED_WHILE_KEPT = ("RemoteDisconnected", "BrokenPipeError", "ConnectionResetError", "ConnectionAbortedError",
|
|
224
|
+
"BadStatusLine")
|
|
225
|
+
_kept = {} # base url -> (HTTP, lock), oldest first
|
|
226
|
+
_kept_lock = threading.Lock()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def read_kept(base_url, path, timeout=2.0):
|
|
230
|
+
# Reads only: a kept connection the helper closed while idle is asked once more, which is safe for a GET.
|
|
231
|
+
base = base_url.rstrip("/")
|
|
232
|
+
deadline = Deadline(timeout)
|
|
233
|
+
with _kept_lock:
|
|
234
|
+
entry = _kept.get(base)
|
|
235
|
+
while entry is None and len(_kept) >= KEPT_MAX:
|
|
236
|
+
for address, (old_client, old_lock) in _kept.items():
|
|
237
|
+
if old_lock.acquire(blocking=False):
|
|
238
|
+
del _kept[address]
|
|
239
|
+
old_client.close()
|
|
240
|
+
old_lock.release()
|
|
241
|
+
break
|
|
242
|
+
else:
|
|
243
|
+
break
|
|
244
|
+
if entry is None and len(_kept) < KEPT_MAX:
|
|
245
|
+
entry = (HTTP(base), threading.Lock())
|
|
246
|
+
_kept[base] = entry
|
|
247
|
+
acquired = entry is not None and entry[1].acquire(blocking=False)
|
|
248
|
+
if acquired:
|
|
249
|
+
_kept.pop(base)
|
|
250
|
+
_kept[base] = entry
|
|
251
|
+
if not acquired:
|
|
252
|
+
return request_once(base, "GET", path, timeout=deadline.remaining())
|
|
253
|
+
client, lock = entry
|
|
254
|
+
try:
|
|
255
|
+
reused = client.connection.sock is not None
|
|
256
|
+
try:
|
|
257
|
+
return client.request("GET", path, timeout=deadline.remaining())
|
|
258
|
+
except TransportError as error:
|
|
259
|
+
if not reused or not any(name in str(error) for name in CLOSED_WHILE_KEPT):
|
|
260
|
+
raise
|
|
261
|
+
return client.request("GET", path, timeout=deadline.remaining())
|
|
262
|
+
finally:
|
|
263
|
+
lock.release()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def wda_screenshot(wda_url, timeout=2):
|
|
267
|
+
try:
|
|
268
|
+
value = request_once(wda_url, "GET", "/screenshot", timeout=timeout).get("value")
|
|
269
|
+
return "data:image/png;base64," + value if isinstance(value, str) and value else None
|
|
270
|
+
except Exception:
|
|
271
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Notification triggers: a notification sends its text to a destination, runs a workflow or starts a task."""
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""What one firing does: send to a destination, run a workflow or start a task, once and never retried."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from .. import harness_api, secret_filter
|
|
9
|
+
from ..api_errors import APIError
|
|
10
|
+
from ..notify_triggers.sources import iso
|
|
11
|
+
from . import model
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger("mobster.triggers")
|
|
14
|
+
|
|
15
|
+
TOPIC = "triggers"
|
|
16
|
+
NOTIFICATION_MAX = 1200
|
|
17
|
+
TEXT_MAX = 3000
|
|
18
|
+
DESTINATION_GONE = ("The destination was removed, so nothing was sent. Choose another destination for this trigger "
|
|
19
|
+
"in Connectors.")
|
|
20
|
+
WORKFLOW_GONE = "The workflow was deleted, so it didn't run. Choose another workflow for this trigger."
|
|
21
|
+
TRIGGER_GONE = "The trigger was removed before it could run."
|
|
22
|
+
BUSY = "Another task was running, so this one didn't start. Mobster doesn't queue a notification's run."
|
|
23
|
+
DUPLICATE = "Mobster already started a run for this notification."
|
|
24
|
+
NO_OUTLETS = "Sending isn't available right now. Restart Mobster, then the next match sends."
|
|
25
|
+
NO_WORKFLOWS = "Workflows aren't available right now. Restart Mobster, then the next match runs."
|
|
26
|
+
WORKFLOW_SKIPS = {"skipped_busy": "The workflow was running and one run already waits, so this one didn't start.",
|
|
27
|
+
"skipped_rate_limit": "The workflow already started 6 times in the last hour, "
|
|
28
|
+
"so this one didn't start."}
|
|
29
|
+
SKIP_CODES = {"run_active", "device_busy"}
|
|
30
|
+
OFFLINE_CODES = {"device_unavailable", "shutting_down", "device_not_found"}
|
|
31
|
+
LABELS = {"destination": "Send to a destination", "workflow": "Run a workflow", "task": "Start a task"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def request_key(trigger_id, notification):
|
|
35
|
+
"""The Idempotency-Key for this trigger and notification: [A-Za-z0-9_.:-], at most 128 characters."""
|
|
36
|
+
digest = hashlib.sha256(f"{trigger_id}\n{notification.key}".encode()).hexdigest()[:32]
|
|
37
|
+
return f"trigger:{trigger_id}:{digest}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def event_body(trigger, notification, masked):
|
|
41
|
+
"""What a destination receives."""
|
|
42
|
+
head = notification.title or notification.app
|
|
43
|
+
said = " ".join((notification.body or notification.subtitle or "").split())
|
|
44
|
+
text = f"{trigger['name']}: {head}" + (f": {said}" if said else "")
|
|
45
|
+
when = iso(notification.date)
|
|
46
|
+
fields = {"trigger": trigger["name"], "app": notification.app, "title": notification.title,
|
|
47
|
+
"body": notification.body or notification.subtitle, "date": when}
|
|
48
|
+
body = {"type": "notification.matched", **fields, "masked": bool(masked),
|
|
49
|
+
"runId": None, "status": "completed", "answer": text[:TEXT_MAX], "data": dict(fields), "reason": None,
|
|
50
|
+
"startedAt": when, "finishedAt": when,
|
|
51
|
+
"workflow": {"id": trigger["id"], "name": trigger["name"], "blanks": {}}, "text": text[:TEXT_MAX]}
|
|
52
|
+
from ..outlets import webhook
|
|
53
|
+
return webhook.masked(body)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fenced(notification):
|
|
57
|
+
"""The notification as data, for the end of a workflow's steps or a task's words. Never longer than a run takes."""
|
|
58
|
+
lines = [f"App: {notification.app}", f"From: {'your iPhone' if notification.source == 'iphone' else 'this Mac'}"]
|
|
59
|
+
for label, value in (("Title", notification.title), ("Subtitle", notification.subtitle),
|
|
60
|
+
("Body", notification.body)):
|
|
61
|
+
if value:
|
|
62
|
+
lines.append(f"{label}: {value}")
|
|
63
|
+
text = secret_filter.redact("\n".join(lines))[:NOTIFICATION_MAX]
|
|
64
|
+
return harness_api.fence("The notification that started this", text)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def who(trigger):
|
|
68
|
+
"""The run field ``trigger``: who started it. The run view says "Started by trigger: <name>"."""
|
|
69
|
+
return {"id": trigger["id"], "name": trigger["name"]}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Firer:
|
|
73
|
+
def __init__(self, runtime, store, lookup, *, clock=time.time, publish=None):
|
|
74
|
+
self.runtime = runtime
|
|
75
|
+
self.store = store
|
|
76
|
+
self.lookup = lookup # trigger id -> the saved trigger (dict) or None
|
|
77
|
+
self.clock = clock
|
|
78
|
+
self.publish = publish or getattr(runtime, "publish", None)
|
|
79
|
+
|
|
80
|
+
def act(self, rule, notification, masked=False):
|
|
81
|
+
trigger = self.lookup(rule.name)
|
|
82
|
+
if trigger is None:
|
|
83
|
+
return {"ok": False, "outcome": "skipped", "message": TRIGGER_GONE}
|
|
84
|
+
kind = trigger["action"]["kind"]
|
|
85
|
+
firing = {"id": uuid.uuid4().hex[:12], "trigger_id": trigger["id"], "trigger_name": trigger["name"],
|
|
86
|
+
"app": trigger["app"], "at": self.clock(), "kind": kind, "outcome": "sending", "message": None,
|
|
87
|
+
"run_id": None}
|
|
88
|
+
self.store.add_firing(firing) # before anything is sent
|
|
89
|
+
try:
|
|
90
|
+
outcome, message, run_id = self._do(trigger, notification, masked)
|
|
91
|
+
except Exception as error: # noqa: BLE001 -- one firing's failure never stops the watcher
|
|
92
|
+
log.exception("triggers: a firing failed unexpectedly")
|
|
93
|
+
outcome, message, run_id = "failed", f"The action failed unexpectedly ({type(error).__name__}).", None
|
|
94
|
+
self.store.update_firing(firing["id"], outcome=outcome, message=message, run_id=run_id)
|
|
95
|
+
self.announce(firing["id"], trigger, outcome, message, run_id)
|
|
96
|
+
return {"ok": outcome == "done", "outcome": outcome, "message": message, "runId": run_id}
|
|
97
|
+
|
|
98
|
+
def announce(self, firing_id, trigger, outcome, message, run_id=None, reason=None):
|
|
99
|
+
event = {"event": "trigger_fired" if outcome in ("done", "failed") else "trigger_skipped",
|
|
100
|
+
"firingId": firing_id, "triggerId": trigger["id"], "trigger": trigger["name"], "app": trigger["app"],
|
|
101
|
+
"action": trigger["action"]["kind"], "outcome": outcome, "message": message, "runId": run_id}
|
|
102
|
+
if reason:
|
|
103
|
+
event["reason"] = reason
|
|
104
|
+
if callable(self.publish):
|
|
105
|
+
self.publish(TOPIC, event)
|
|
106
|
+
|
|
107
|
+
def skip(self, rule_name, name, app, reason, message):
|
|
108
|
+
"""A refusal before any action (rate limit, loop guard, duplicate, access lost): journaled and announced."""
|
|
109
|
+
trigger = self.lookup(rule_name) or {"id": rule_name, "name": name or "A removed trigger", "app": app or "",
|
|
110
|
+
"action": {"kind": "destination"}}
|
|
111
|
+
firing = {"id": uuid.uuid4().hex[:12], "trigger_id": trigger["id"], "trigger_name": trigger["name"],
|
|
112
|
+
"app": trigger["app"], "at": self.clock(), "kind": trigger["action"]["kind"], "outcome": "skipped",
|
|
113
|
+
"message": message, "run_id": None}
|
|
114
|
+
try:
|
|
115
|
+
self.store.add_firing(firing)
|
|
116
|
+
except Exception: # noqa: BLE001 -- the event still goes out
|
|
117
|
+
log.warning("triggers: couldn't journal a skipped firing")
|
|
118
|
+
self.announce(firing["id"], trigger, "skipped", message, reason=reason)
|
|
119
|
+
|
|
120
|
+
def _do(self, trigger, notification, masked):
|
|
121
|
+
action = trigger["action"]
|
|
122
|
+
if action["kind"] == "destination":
|
|
123
|
+
return self._destination(trigger, notification, masked)
|
|
124
|
+
if action["kind"] == "workflow":
|
|
125
|
+
return self._workflow(trigger, notification)
|
|
126
|
+
return self._task(trigger, notification)
|
|
127
|
+
|
|
128
|
+
def _destination(self, trigger, notification, masked):
|
|
129
|
+
from ..outlets import service as outlet_service
|
|
130
|
+
service = outlet_service.service_for(self.runtime)
|
|
131
|
+
if service is None:
|
|
132
|
+
return "skipped", NO_OUTLETS, None
|
|
133
|
+
try:
|
|
134
|
+
row = service.store.destination(trigger["action"]["outletId"])
|
|
135
|
+
if row is None:
|
|
136
|
+
return "skipped", DESTINATION_GONE, None
|
|
137
|
+
outcome = service.send_event(row["id"], event_body(trigger, notification, masked))
|
|
138
|
+
except APIError as error:
|
|
139
|
+
if error.code == "not_found":
|
|
140
|
+
return "skipped", DESTINATION_GONE, None
|
|
141
|
+
return "failed", str(error), None
|
|
142
|
+
if outcome.ok:
|
|
143
|
+
return "done", f"Sent to {row['name']}.", None
|
|
144
|
+
return "failed", (outcome.message or "It didn't arrive.") + " Mobster doesn't send it again.", None
|
|
145
|
+
|
|
146
|
+
def _workflow(self, trigger, notification):
|
|
147
|
+
workflows = getattr(self.runtime, "workflows", None)
|
|
148
|
+
if workflows is None:
|
|
149
|
+
return "skipped", NO_WORKFLOWS, None
|
|
150
|
+
workflow_id = trigger["action"]["workflowId"]
|
|
151
|
+
try:
|
|
152
|
+
workflows.get(workflow_id)
|
|
153
|
+
except APIError as error:
|
|
154
|
+
if error.status == 404:
|
|
155
|
+
return "skipped", WORKFLOW_GONE, None
|
|
156
|
+
return "failed", str(error), None
|
|
157
|
+
data = {"app": notification.app, "title": notification.title,
|
|
158
|
+
"body": notification.body or notification.subtitle,
|
|
159
|
+
"receivedAt": round((notification.date or self.clock()) * 1000)}
|
|
160
|
+
try:
|
|
161
|
+
# The engine masks, cuts and fences the fields; each task it starts says "Started by trigger: <name>".
|
|
162
|
+
outcome = workflows.triggers.fire(workflow_id, data, started_by=who(trigger))
|
|
163
|
+
except APIError as error:
|
|
164
|
+
if error.code == "not_published":
|
|
165
|
+
return "skipped", str(error), None
|
|
166
|
+
return self._refused(error)
|
|
167
|
+
except ValueError as error:
|
|
168
|
+
return "skipped", str(error), None
|
|
169
|
+
if outcome is None:
|
|
170
|
+
return "skipped", DUPLICATE, None
|
|
171
|
+
if outcome == "started":
|
|
172
|
+
return "done", "Started the workflow.", None
|
|
173
|
+
if outcome == "queued":
|
|
174
|
+
return "done", "The workflow was running, so it starts when that run ends.", None
|
|
175
|
+
if outcome in WORKFLOW_SKIPS:
|
|
176
|
+
return "skipped", WORKFLOW_SKIPS[outcome], None
|
|
177
|
+
return self._refused(APIError("The workflow didn't start.", 409, outcome))
|
|
178
|
+
|
|
179
|
+
def _task(self, trigger, notification):
|
|
180
|
+
action = trigger["action"]
|
|
181
|
+
if not action.get("allowed"):
|
|
182
|
+
return "skipped", model.TASK_OFF, None
|
|
183
|
+
goal = action["text"] + "\n\n" + fenced(notification)
|
|
184
|
+
kwargs = {"extras": {"trigger": who(trigger)}, "origin": "trigger"}
|
|
185
|
+
if getattr(self.runtime, "fleet", None) is not None:
|
|
186
|
+
kwargs["default_device"] = "primary"
|
|
187
|
+
try:
|
|
188
|
+
created = self.runtime.create(None, goal, "live", request_key(trigger["id"], notification), **kwargs)
|
|
189
|
+
except APIError as error:
|
|
190
|
+
return self._refused(error)
|
|
191
|
+
except ValueError as error:
|
|
192
|
+
return "skipped", str(error), None
|
|
193
|
+
run, replayed = created
|
|
194
|
+
if replayed:
|
|
195
|
+
return "skipped", DUPLICATE, getattr(run, "id", None)
|
|
196
|
+
return "done", "Started a task. It asks before it acts.", getattr(run, "id", None)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _refused(error):
|
|
200
|
+
if error.code in SKIP_CODES:
|
|
201
|
+
return "skipped", BUSY, None
|
|
202
|
+
if error.code in OFFLINE_CODES:
|
|
203
|
+
return "skipped", f"{error} Nothing was started.", None
|
|
204
|
+
return "failed", str(error), None
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""A trigger's fields, checked. Every refusal is a sentence that says what to change."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..notify_triggers.rules import own_app
|
|
6
|
+
|
|
7
|
+
NAME_MAX = 60
|
|
8
|
+
WORDS_MAX = 10
|
|
9
|
+
WORD_MAX = 40
|
|
10
|
+
TASK_MAX = 1000
|
|
11
|
+
MAX_TRIGGERS = 50
|
|
12
|
+
SOURCES = ("iphone", "mac", "any")
|
|
13
|
+
ACTIONS = ("destination", "workflow", "task")
|
|
14
|
+
BUNDLE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,158}[A-Za-z0-9_]")
|
|
15
|
+
ID = re.compile(r"[0-9a-f]{12}")
|
|
16
|
+
FIELDS = {"name", "enabled", "app", "appName", "words", "source", "action"}
|
|
17
|
+
ACTION_FIELDS = {"destination": {"kind", "outletId"}, "workflow": {"kind", "workflowId"},
|
|
18
|
+
"task": {"kind", "text", "allowed"}}
|
|
19
|
+
TASK_OFF = ("Starting a task from a notification is off for this trigger. Turn it on in the trigger's settings, "
|
|
20
|
+
"then it runs and asks before acting.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def name_of(value):
|
|
24
|
+
text = " ".join(value.split()) if isinstance(value, str) else ""
|
|
25
|
+
if not 1 <= len(text) <= NAME_MAX:
|
|
26
|
+
raise ValueError(f"Give the trigger a name of 1 to {NAME_MAX} characters.")
|
|
27
|
+
return text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def app_of(value):
|
|
31
|
+
app = value.strip() if isinstance(value, str) else ""
|
|
32
|
+
if not BUNDLE.fullmatch(app):
|
|
33
|
+
raise ValueError("Choose the iPhone app this trigger watches.")
|
|
34
|
+
if own_app(app) or app.lower().startswith("app.mobster"):
|
|
35
|
+
raise ValueError("A trigger can't watch Mobster's own notifications, because the notification it sends "
|
|
36
|
+
"would start it again.")
|
|
37
|
+
return app
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def words_of(value):
|
|
41
|
+
"""The words, as a list of 0 to 10 short phrases. Text with commas or new lines splits into words."""
|
|
42
|
+
if value is None:
|
|
43
|
+
return []
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
value = re.split(r"[,\n]", value)
|
|
46
|
+
if not isinstance(value, list) or not all(isinstance(word, str) for word in value):
|
|
47
|
+
raise ValueError("Words is a list of words or phrases to look for in the notification.")
|
|
48
|
+
words = []
|
|
49
|
+
for word in value:
|
|
50
|
+
word = " ".join(word.split())
|
|
51
|
+
if not word:
|
|
52
|
+
continue
|
|
53
|
+
if len(word) > WORD_MAX:
|
|
54
|
+
raise ValueError(f"A word or phrase is at most {WORD_MAX} characters.")
|
|
55
|
+
if word.casefold() not in [w.casefold() for w in words]:
|
|
56
|
+
words.append(word)
|
|
57
|
+
if len(words) > WORDS_MAX:
|
|
58
|
+
raise ValueError(f"Use at most {WORDS_MAX} words or phrases.")
|
|
59
|
+
return words
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def source_of(value):
|
|
63
|
+
if value is None:
|
|
64
|
+
return "iphone"
|
|
65
|
+
if value not in SOURCES:
|
|
66
|
+
raise ValueError("Source is iphone, mac or any.")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def enabled_of(value):
|
|
71
|
+
if value is None:
|
|
72
|
+
return False
|
|
73
|
+
if type(value) is not bool:
|
|
74
|
+
raise ValueError("Enabled is true or false.")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def action_of(value):
|
|
79
|
+
"""The action, cleaned. It names its destination or workflow by id;
|
|
80
|
+
whether that exists is the service's to check."""
|
|
81
|
+
if not isinstance(value, dict) or value.get("kind") not in ACTIONS:
|
|
82
|
+
raise ValueError("Choose what the trigger does: send to a destination, run a workflow or start a task.")
|
|
83
|
+
kind = value["kind"]
|
|
84
|
+
extra = set(value) - ACTION_FIELDS[kind]
|
|
85
|
+
if extra:
|
|
86
|
+
raise ValueError(f"A {kind} action doesn't take {', '.join(sorted(extra))}.")
|
|
87
|
+
if kind == "destination":
|
|
88
|
+
if not isinstance(value.get("outletId"), str) or not ID.fullmatch(value["outletId"]):
|
|
89
|
+
raise ValueError("Choose a destination you saved in Connectors.")
|
|
90
|
+
return {"kind": kind, "outletId": value["outletId"]}
|
|
91
|
+
if kind == "workflow":
|
|
92
|
+
if not isinstance(value.get("workflowId"), str) or not ID.fullmatch(value["workflowId"]):
|
|
93
|
+
raise ValueError("Choose a saved workflow.")
|
|
94
|
+
return {"kind": kind, "workflowId": value["workflowId"]}
|
|
95
|
+
text = value.get("text")
|
|
96
|
+
if not isinstance(text, str) or not 1 <= len(text.strip()) <= TASK_MAX:
|
|
97
|
+
raise ValueError(f"Write the task in 1 to {TASK_MAX} characters.")
|
|
98
|
+
allowed = value.get("allowed", False)
|
|
99
|
+
if type(allowed) is not bool:
|
|
100
|
+
raise ValueError("Allowed is true or false.")
|
|
101
|
+
return {"kind": kind, "text": text.strip(), "allowed": allowed}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def clean(body, current=None):
|
|
105
|
+
"""The fields to save from a create or update body."""
|
|
106
|
+
if not isinstance(body, dict):
|
|
107
|
+
raise ValueError("A trigger has a name, an app and an action.")
|
|
108
|
+
extra = set(body) - FIELDS
|
|
109
|
+
if extra:
|
|
110
|
+
raise ValueError(f"A trigger doesn't take {', '.join(sorted(extra))}.")
|
|
111
|
+
out = dict(current or {})
|
|
112
|
+
if current is None or "name" in body:
|
|
113
|
+
out["name"] = name_of(body.get("name"))
|
|
114
|
+
if current is None or "app" in body:
|
|
115
|
+
out["app"] = app_of(body.get("app"))
|
|
116
|
+
if "appName" in body or current is None:
|
|
117
|
+
name = body.get("appName")
|
|
118
|
+
if name is not None and (not isinstance(name, str) or len(name.strip()) > 80):
|
|
119
|
+
raise ValueError("The app's name is at most 80 characters.")
|
|
120
|
+
out["appName"] = " ".join(name.split()) if isinstance(name, str) and name.strip() else None
|
|
121
|
+
if current is None or "words" in body:
|
|
122
|
+
out["words"] = words_of(body.get("words"))
|
|
123
|
+
if current is None or "source" in body:
|
|
124
|
+
out["source"] = source_of(body.get("source"))
|
|
125
|
+
if current is None or "enabled" in body:
|
|
126
|
+
out["enabled"] = enabled_of(body.get("enabled"))
|
|
127
|
+
if current is None or "action" in body:
|
|
128
|
+
action = action_of(body.get("action"))
|
|
129
|
+
# "Start a task" is off until the person turns it on for this trigger. A change that leaves "allowed" out keeps
|
|
130
|
+
# what was saved, unless the task's words changed: new words are off until they are turned on again.
|
|
131
|
+
before = (current or {}).get("action") or {}
|
|
132
|
+
if action["kind"] == "task" and "allowed" not in body["action"]:
|
|
133
|
+
same = before.get("kind") == "task" and before.get("text") == action["text"]
|
|
134
|
+
action["allowed"] = bool(before.get("allowed")) if same else False
|
|
135
|
+
out["action"] = action
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def word_pattern(words, regex=None):
|
|
140
|
+
"""The compiled search for a trigger"""
|
|
141
|
+
if regex:
|
|
142
|
+
return re.compile(regex, re.IGNORECASE | re.MULTILINE)
|
|
143
|
+
if not words:
|
|
144
|
+
return None
|
|
145
|
+
alternatives = "|".join(re.escape(" ".join(word.split())).replace(r"\ ", r"\s+") for word in words)
|
|
146
|
+
return re.compile(r"(?<!\w)(?:" + alternatives + r")(?!\w)", re.IGNORECASE)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Track ``triggers``' registrations (tracks.load calls ``register(api)`` once per process, staged)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from . import routes, service
|
|
6
|
+
|
|
7
|
+
ID = re.compile(r"[0-9a-f]{12}")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_trigger(value, runtime):
|
|
11
|
+
"""The run field: {"id": 12 hex characters, "name": 1 to 60 characters}."""
|
|
12
|
+
if not isinstance(value, dict) or set(value) != {"id", "name"}:
|
|
13
|
+
raise ValueError('trigger is {"id", "name"}')
|
|
14
|
+
if not isinstance(value["id"], str) or not ID.fullmatch(value["id"]):
|
|
15
|
+
raise ValueError("A trigger's id is 12 hex characters")
|
|
16
|
+
if not isinstance(value["name"], str) or not 1 <= len(value["name"].strip()) <= 60:
|
|
17
|
+
raise ValueError("A trigger's name is 1 to 60 characters")
|
|
18
|
+
return {"id": value["id"], "name": " ".join(value["name"].split())}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register(api):
|
|
22
|
+
"""``api`` is mobile_agent.harness_api."""
|
|
23
|
+
from .. import full_disk_access, journal
|
|
24
|
+
from . import store
|
|
25
|
+
journal.register_schema(store.SCHEMA, store.VERSION, store.migrate)
|
|
26
|
+
api.register_run_field("trigger", validate_trigger, public=True)
|
|
27
|
+
api.register_service(service.start, service.close)
|
|
28
|
+
routes.register_routes()
|
|
29
|
+
full_disk_access.register_route()
|