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,580 @@
|
|
|
1
|
+
"""Destinations and the delivery worker: one OutletService per Runtime (harness_api.register_service)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
from .. import harness_api
|
|
12
|
+
from ..api_errors import APIError
|
|
13
|
+
from ..connectors import oauth
|
|
14
|
+
from ..service_support import ServiceRegistry, ms
|
|
15
|
+
from . import connect as outlet_connect
|
|
16
|
+
from . import kinds as outlet_kinds
|
|
17
|
+
from . import net, webhook
|
|
18
|
+
from . import store as outlet_store
|
|
19
|
+
from . import vault as outlet_vault
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("mobster.outlets")
|
|
22
|
+
|
|
23
|
+
ATTEMPTS = 5
|
|
24
|
+
BACKOFF = (10.0, 60.0, 300.0, 900.0) # seconds before the 2nd, 3rd, 4th and 5th tries
|
|
25
|
+
JITTER = 0.2 # each pause is BACKOFF ± 20%, so a receiver that fell over isn't hit at once
|
|
26
|
+
ID = re.compile(r"[0-9a-f]{12}")
|
|
27
|
+
REMOVED = "That destination was removed. Choose another one."
|
|
28
|
+
GONE_RUN = "That task is no longer in History."
|
|
29
|
+
NO_ADDRESS = "Mobster couldn't find this destination's address or sign-in. Add it again in Settings › Connectors."
|
|
30
|
+
KEYCHAIN = "Mobster couldn't read this destination from the Keychain. Unlock your Mac's Keychain, then resend."
|
|
31
|
+
UNKNOWN = "Mobster quit while sending this, so it may have arrived. Check {name}, then resend if it's missing."
|
|
32
|
+
|
|
33
|
+
_services = ServiceRegistry()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def service_for(runtime):
|
|
37
|
+
return _services.get(runtime)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def services():
|
|
41
|
+
return _services.all()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def start(runtime):
|
|
45
|
+
"""register_service's start: an OutletService for the runtime; picks up deliveries an earlier launch left."""
|
|
46
|
+
service = OutletService(runtime)
|
|
47
|
+
_services.add(runtime, service)
|
|
48
|
+
service.start()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def close(runtime):
|
|
52
|
+
service = _services.pop(runtime)
|
|
53
|
+
if service is not None:
|
|
54
|
+
service.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OutletService:
|
|
58
|
+
def __init__(self, runtime, *, vault=None, post=None, call=None, clock=time.time, backoff=BACKOFF,
|
|
59
|
+
attempts=ATTEMPTS, jitter=JITTER, rand=random.random, connectors=None, refresh=oauth.refresh,
|
|
60
|
+
apps=None, sign_in=oauth.SignIn):
|
|
61
|
+
self.runtime = runtime
|
|
62
|
+
self.store = outlet_store.OutletStore(runtime.journal)
|
|
63
|
+
self.vault = vault if vault is not None else outlet_vault.backend()
|
|
64
|
+
self.post = post or webhook.post
|
|
65
|
+
self.call = call or net.call
|
|
66
|
+
self.clock = clock
|
|
67
|
+
self.refresh = refresh # renews a Notion or Google sign-in through the license API
|
|
68
|
+
self.connectors = connectors or self._connectors
|
|
69
|
+
self.connects = outlet_connect.Connects(call=self.call, connectors=self.connectors, apps=apps, sign_in=sign_in)
|
|
70
|
+
self.backoff = tuple(backoff)
|
|
71
|
+
self.attempts = attempts
|
|
72
|
+
self.jitter, self.rand = jitter, rand
|
|
73
|
+
self._lock = threading.Condition()
|
|
74
|
+
self._due = {} # delivery id -> when to try it (seconds)
|
|
75
|
+
self._thread = None
|
|
76
|
+
self._closed = False
|
|
77
|
+
self._busy = set() # destination ids with a test in flight
|
|
78
|
+
self._trying = {} # destination id -> threading.Lock: one try (a test or a delivery) at a time
|
|
79
|
+
|
|
80
|
+
def start(self):
|
|
81
|
+
try:
|
|
82
|
+
pending = self.store.pending()
|
|
83
|
+
except Exception: # noqa: BLE001 -- a journal that can't be read leaves nothing to pick up
|
|
84
|
+
pending = []
|
|
85
|
+
now = self.clock()
|
|
86
|
+
# A try under way when Mobster quit, to a kind that can't take a result twice: its outcome is unknown, so it
|
|
87
|
+
# fails and says to check.
|
|
88
|
+
for row in [r for r in pending if r.get("inflight_at")]:
|
|
89
|
+
pending.remove(row)
|
|
90
|
+
try:
|
|
91
|
+
self.store.update_delivery(row["id"], status="failed", inflight_at=None, next_at=None, updated_at=now,
|
|
92
|
+
error=UNKNOWN.format(name=row["destination_name"]))
|
|
93
|
+
except Exception: # noqa: BLE001
|
|
94
|
+
log.warning("outlets: couldn't close a delivery whose try was under way")
|
|
95
|
+
with self._lock:
|
|
96
|
+
for row in pending:
|
|
97
|
+
self._due[row["id"]] = min(row["next_at"] or now, now)
|
|
98
|
+
journal = getattr(self.runtime, "journal", None)
|
|
99
|
+
if journal is not None and callable(getattr(journal, "add_expiry_listener", None)):
|
|
100
|
+
journal.add_expiry_listener(self._forget_runs)
|
|
101
|
+
if pending:
|
|
102
|
+
self._ensure()
|
|
103
|
+
|
|
104
|
+
def close(self, timeout=1.5):
|
|
105
|
+
self.connects.close()
|
|
106
|
+
with self._lock:
|
|
107
|
+
self._closed = True
|
|
108
|
+
self._lock.notify_all()
|
|
109
|
+
thread = self._thread
|
|
110
|
+
if thread is not None and thread is not threading.current_thread():
|
|
111
|
+
thread.join(timeout)
|
|
112
|
+
|
|
113
|
+
def _ensure(self):
|
|
114
|
+
with self._lock:
|
|
115
|
+
if self._closed or (self._thread is not None and self._thread.is_alive()):
|
|
116
|
+
return
|
|
117
|
+
self._thread = threading.Thread(target=self._loop, name="mobster-outlets", daemon=True)
|
|
118
|
+
self._thread.start()
|
|
119
|
+
|
|
120
|
+
def _forget_runs(self, run_ids):
|
|
121
|
+
for run_id in run_ids:
|
|
122
|
+
try:
|
|
123
|
+
self.store.forget_run(run_id)
|
|
124
|
+
except Exception: # noqa: BLE001
|
|
125
|
+
log.warning("outlets: couldn't forget a removed run's deliveries")
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def public(row):
|
|
129
|
+
test = row.get("last_test")
|
|
130
|
+
kind = row.get("kind") or "webhook"
|
|
131
|
+
config = row.get("config") or {}
|
|
132
|
+
shown = {key: config[key] for key in ("folder", "file", "format", "database", "title", "preset", "workspace",
|
|
133
|
+
"channel", "channelName", "server", "spreadsheet") if key in config}
|
|
134
|
+
return {"id": row["id"], "name": row["name"], "kind": kind, "config": shown, "hint": row["hint"],
|
|
135
|
+
"events": config.get("events") or ["run.finished"], "scope": config.get("scope") or "named",
|
|
136
|
+
"hasSecret": row["has_secret"],
|
|
137
|
+
"screenshot": row["screenshot"], "createdAt": ms(row["created_at"]),
|
|
138
|
+
"lastTest": {"ok": bool(test.get("ok")), "at": ms(test.get("at")), "status": test.get("status"),
|
|
139
|
+
"message": test.get("message")} if isinstance(test, dict) else None}
|
|
140
|
+
|
|
141
|
+
def list(self):
|
|
142
|
+
used = {}
|
|
143
|
+
for flow in self._workflows():
|
|
144
|
+
if flow.get("sendTo"):
|
|
145
|
+
used[flow["sendTo"]] = used.get(flow["sendTo"], 0) + 1
|
|
146
|
+
return [{**self.public(row), "workflows": used.get(row["id"], 0)} for row in self.store.destinations()]
|
|
147
|
+
|
|
148
|
+
def kinds(self):
|
|
149
|
+
"""The kinds the app may offer to add now, in kinds.py's order (Connects.offered)."""
|
|
150
|
+
return [kind_id for kind_id, kind in outlet_kinds.all_kinds().items() if self.connects.offered(kind)]
|
|
151
|
+
|
|
152
|
+
def _workflows(self):
|
|
153
|
+
workflows = getattr(self.runtime, "workflows", None)
|
|
154
|
+
try:
|
|
155
|
+
return list(workflows.list()) if workflows is not None else []
|
|
156
|
+
except Exception: # noqa: BLE001 -- the list stands without its counts
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
def find(self, wanted):
|
|
160
|
+
"""A destination by its id, else by its name (case and spacing ignored). ValueError when none or two match."""
|
|
161
|
+
if not isinstance(wanted, str) or not wanted.strip():
|
|
162
|
+
raise ValueError("Name a saved destination, or give its id")
|
|
163
|
+
wanted = wanted.strip()
|
|
164
|
+
rows = self.store.destinations()
|
|
165
|
+
found = [r for r in rows if r["id"] == wanted.lower()] or [
|
|
166
|
+
r for r in rows if " ".join(r["name"].split()).casefold() == " ".join(wanted.split()).casefold()]
|
|
167
|
+
if not found:
|
|
168
|
+
names = ", ".join(r["name"] for r in rows[:8])
|
|
169
|
+
raise ValueError(f"No destination is called {wanted[:60]}. "
|
|
170
|
+
+ (f"You have: {names}." if names else "Add one in Settings › Connectors."))
|
|
171
|
+
if len(found) > 1:
|
|
172
|
+
raise ValueError(f"Several destinations are called {wanted[:60]}. Use its id: "
|
|
173
|
+
+ ", ".join(r["id"] for r in found))
|
|
174
|
+
return found[0]
|
|
175
|
+
|
|
176
|
+
def _get(self, destination_id):
|
|
177
|
+
row = self.store.destination(destination_id) if isinstance(destination_id, str) else None
|
|
178
|
+
if row is None:
|
|
179
|
+
raise APIError("That destination was removed.", 404, "not_found")
|
|
180
|
+
return row
|
|
181
|
+
|
|
182
|
+
def _unique(self, name, but=None):
|
|
183
|
+
for row in self.store.destinations():
|
|
184
|
+
if row["id"] != but and row["name"].casefold() == name.casefold():
|
|
185
|
+
raise ValueError(f"You already have a destination called {name}. Give this one another name.")
|
|
186
|
+
|
|
187
|
+
def _connectors(self):
|
|
188
|
+
"""The ConnectorService a Slack destination posts through, or None (connectors/service.py)."""
|
|
189
|
+
try:
|
|
190
|
+
from ..connectors import service as connector_service
|
|
191
|
+
except ImportError:
|
|
192
|
+
return None
|
|
193
|
+
return connector_service.service_for(self.runtime)
|
|
194
|
+
|
|
195
|
+
def connect(self, body):
|
|
196
|
+
"""Start connecting a destination: {"connect", "authorizeUrl"?} (connect.py)."""
|
|
197
|
+
if not isinstance(body, dict) or set(body) != {"kind"}:
|
|
198
|
+
raise ValueError('Say which kind to connect: {"kind": "slack" | "discord" | "notion" | "sheets"}')
|
|
199
|
+
return self.connects.begin(self._kind(body["kind"]))
|
|
200
|
+
|
|
201
|
+
def _resolved(self, kind, body):
|
|
202
|
+
"""``body`` with its ``connect`` id replaced by what that connect found."""
|
|
203
|
+
if "connect" not in body:
|
|
204
|
+
return body
|
|
205
|
+
return {**body, "connect": self.connects.found(body["connect"], kind.id)}
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _kind(value):
|
|
209
|
+
if value is not None and not isinstance(value, str):
|
|
210
|
+
raise ValueError("A destination's kind is a word, like webhook")
|
|
211
|
+
return outlet_kinds.get(value)
|
|
212
|
+
|
|
213
|
+
def _keychain_put(self, destination_id, item, then):
|
|
214
|
+
try:
|
|
215
|
+
self.vault.put(destination_id, item.get("url"), item.get("secret"),
|
|
216
|
+
{key: item[key] for key in outlet_vault.EXTRA if item.get(key)})
|
|
217
|
+
except outlet_vault.VaultError as error:
|
|
218
|
+
raise APIError(f"{error}. {then}", 503, "keychain_unavailable") from None
|
|
219
|
+
|
|
220
|
+
def add(self, body):
|
|
221
|
+
if not isinstance(body, dict):
|
|
222
|
+
raise ValueError("A destination has a name and where it sends")
|
|
223
|
+
kind = self._kind(body.get("kind"))
|
|
224
|
+
if set(body) - set(outlet_kinds.FIELDS) - set(kind.fields):
|
|
225
|
+
raise ValueError("A destination has a name, its kind, and " + ", ".join(kind.fields))
|
|
226
|
+
name = webhook.validate_name(body.get("name"), kind.example)
|
|
227
|
+
self._unique(name)
|
|
228
|
+
if self.store.count() >= outlet_store.MAX_DESTINATIONS:
|
|
229
|
+
raise APIError(f"You have {outlet_store.MAX_DESTINATIONS} destinations. Remove one first.", 409,
|
|
230
|
+
"destination_limit")
|
|
231
|
+
subscribed = outlet_kinds.subscription(body, kind.id)
|
|
232
|
+
setup = kind.setup(self._resolved(kind, {k: v for k, v in body.items() if k not in ("events", "scope")}))
|
|
233
|
+
destination_id = uuid.uuid4().hex[:12]
|
|
234
|
+
if setup.get("vault"):
|
|
235
|
+
self._keychain_put(destination_id, setup["vault"], "Nothing was saved.")
|
|
236
|
+
now = self.clock()
|
|
237
|
+
try:
|
|
238
|
+
row = self.store.add_destination({"id": destination_id, "name": name, "kind": kind.id,
|
|
239
|
+
"hint": setup["hint"],
|
|
240
|
+
"config": {**(setup.get("config") or {}), **subscribed},
|
|
241
|
+
"has_secret": setup.get("has_secret", False),
|
|
242
|
+
"screenshot": setup.get("screenshot", False),
|
|
243
|
+
"created_at": now, "updated_at": now})
|
|
244
|
+
except Exception:
|
|
245
|
+
self._drop_secret(destination_id)
|
|
246
|
+
raise
|
|
247
|
+
self.connects.drop(body.get("connect"))
|
|
248
|
+
return self.public(row)
|
|
249
|
+
|
|
250
|
+
def update(self, destination_id, body):
|
|
251
|
+
row = self._get(destination_id)
|
|
252
|
+
kind = outlet_kinds.get(row["kind"])
|
|
253
|
+
if not isinstance(body, dict) or not body or set(body) - set(outlet_kinds.FIELDS) - set(kind.fields):
|
|
254
|
+
raise ValueError("Change the name or " + ", ".join(kind.fields))
|
|
255
|
+
if "kind" in body and body["kind"] != kind.id:
|
|
256
|
+
raise ValueError("A destination keeps its kind. Add a new one instead.")
|
|
257
|
+
changes = {}
|
|
258
|
+
if "name" in body:
|
|
259
|
+
changes["name"] = webhook.validate_name(body["name"], kind.example)
|
|
260
|
+
self._unique(changes["name"], but=row["id"])
|
|
261
|
+
|
|
262
|
+
def saved():
|
|
263
|
+
try:
|
|
264
|
+
return self.vault.get(row["id"])
|
|
265
|
+
except outlet_vault.VaultError as error:
|
|
266
|
+
raise APIError(f"{error}. Nothing was changed.", 503, "keychain_unavailable") from None
|
|
267
|
+
fields = {k: v for k, v in body.items() if k not in outlet_kinds.FIELDS}
|
|
268
|
+
setup = kind.setup(self._resolved(kind, fields), {"row": row, "saved": saved})
|
|
269
|
+
if setup.get("vault"):
|
|
270
|
+
self._keychain_put(row["id"], setup["vault"], "Nothing was changed.")
|
|
271
|
+
changes.update({key: value for key, value in setup.items() if key != "vault"})
|
|
272
|
+
if "config" in changes or "events" in body or "scope" in body:
|
|
273
|
+
# A new config from setup keeps what the destination takes and for which tasks.
|
|
274
|
+
base = changes.get("config", row["config"])
|
|
275
|
+
changes["config"] = {**base, **outlet_kinds.subscription(body, kind.id, row["config"])}
|
|
276
|
+
changes["updated_at"] = self.clock()
|
|
277
|
+
updated = self.public(self.store.update_destination(row["id"], **changes))
|
|
278
|
+
self.connects.drop(body.get("connect"))
|
|
279
|
+
return updated
|
|
280
|
+
|
|
281
|
+
def delete(self, destination_id):
|
|
282
|
+
"""Remove a destination: its Keychain item goes, workflows that sent there stop sending, and deliveries on their
|
|
283
|
+
way to it fail."""
|
|
284
|
+
row = self._get(destination_id)
|
|
285
|
+
self.store.delete_destination(row["id"])
|
|
286
|
+
self._drop_secret(row["id"])
|
|
287
|
+
workflows = getattr(self.runtime, "workflows", None)
|
|
288
|
+
for flow in self._workflows():
|
|
289
|
+
if flow.get("sendTo") == row["id"]:
|
|
290
|
+
try:
|
|
291
|
+
workflows.update(flow["id"], {"sendTo": None, "revision": flow["revision"]})
|
|
292
|
+
except Exception: # noqa: BLE001 -- its next run finds the destination gone and sends nothing
|
|
293
|
+
log.warning("outlets: a workflow kept a removed destination")
|
|
294
|
+
return row["id"]
|
|
295
|
+
|
|
296
|
+
def _drop_secret(self, destination_id):
|
|
297
|
+
try:
|
|
298
|
+
self.vault.delete(destination_id)
|
|
299
|
+
except outlet_vault.VaultError:
|
|
300
|
+
log.warning("outlets: the Keychain kept a removed destination's item")
|
|
301
|
+
|
|
302
|
+
def _saved(self, row):
|
|
303
|
+
"""{"url", "secret", ...} of a destination; APIError when the Keychain can't give it."""
|
|
304
|
+
if not outlet_kinds.get(row.get("kind")).keychain:
|
|
305
|
+
return {"url": None, "secret": None}
|
|
306
|
+
try:
|
|
307
|
+
saved = self.vault.get(row["id"])
|
|
308
|
+
except outlet_vault.VaultError:
|
|
309
|
+
raise APIError(KEYCHAIN, 503, "keychain_unavailable") from None
|
|
310
|
+
if saved is None:
|
|
311
|
+
raise APIError(NO_ADDRESS, 409, "address_missing")
|
|
312
|
+
return saved
|
|
313
|
+
|
|
314
|
+
def _context(self, row, saved, delivery_id, event):
|
|
315
|
+
def keep(tokens):
|
|
316
|
+
"""A renewed sign-in, saved before it's used: a vendor that rotates refresh tokens takes each once."""
|
|
317
|
+
saved["oauth"] = tokens
|
|
318
|
+
try:
|
|
319
|
+
self.vault.put(row["id"], saved.get("url"), saved.get("secret"),
|
|
320
|
+
{key: saved[key] for key in outlet_vault.EXTRA if saved.get(key)})
|
|
321
|
+
except outlet_vault.VaultError:
|
|
322
|
+
log.warning("outlets: the Keychain didn't keep a renewed sign-in")
|
|
323
|
+
return SimpleNamespace(destination=row, saved=saved, delivery_id=delivery_id, event=event, now=self.clock(),
|
|
324
|
+
wall=self.clock, post=self.post, call=self.call, refresh=self.refresh, keep=keep,
|
|
325
|
+
connectors=self.connectors)
|
|
326
|
+
|
|
327
|
+
def _one_try(self, destination_id):
|
|
328
|
+
"""One try at a time per destination, so a test and a delivery never renew the same token at once."""
|
|
329
|
+
with self._lock:
|
|
330
|
+
return self._trying.setdefault(destination_id, threading.Lock())
|
|
331
|
+
|
|
332
|
+
def test(self, destination_id):
|
|
333
|
+
"""One test, now, the way the destination's kind tests (a webhook gets the test body; a folder is checked
|
|
334
|
+
writable; Notion's database is read), kept on the destination."""
|
|
335
|
+
row = self._get(destination_id)
|
|
336
|
+
kind = outlet_kinds.get(row["kind"])
|
|
337
|
+
with self._lock:
|
|
338
|
+
if row["id"] in self._busy:
|
|
339
|
+
raise APIError("A test is already on its way to this destination.", 409, "test_running")
|
|
340
|
+
self._busy.add(row["id"])
|
|
341
|
+
try:
|
|
342
|
+
with self._one_try(row["id"]):
|
|
343
|
+
saved = self._saved(row)
|
|
344
|
+
outcome = kind.test(self._context(row, saved, uuid.uuid4().hex[:12], "test"))
|
|
345
|
+
finally:
|
|
346
|
+
with self._lock:
|
|
347
|
+
self._busy.discard(row["id"])
|
|
348
|
+
result = {"ok": outcome.ok, "status": outcome.status, "message": outcome.message, "at": self.clock()}
|
|
349
|
+
self.store.update_destination(row["id"], last_test=result)
|
|
350
|
+
return {"ok": outcome.ok, "status": outcome.status, "message": outcome.message}
|
|
351
|
+
|
|
352
|
+
def send_event(self, destination_id, body, event="notification.matched"):
|
|
353
|
+
"""One try, now, of a body that isn't a finished run's result (a notification trigger's match), to a saved
|
|
354
|
+
destination, the way its kind sends."""
|
|
355
|
+
row = self._get(destination_id)
|
|
356
|
+
kind = outlet_kinds.get(row["kind"])
|
|
357
|
+
saved = self._saved(row)
|
|
358
|
+
return kind.send(self._context(row, saved, uuid.uuid4().hex[:12], event), body)
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def public_delivery(row):
|
|
362
|
+
return {"id": row["id"], "runId": row["run_id"], "destinationId": row["destination_id"],
|
|
363
|
+
"destinationName": row["destination_name"], "status": row["status"], "attempts": row["attempts"],
|
|
364
|
+
"httpStatus": row["http_status"], "error": row["error"], "nextAt": ms(row["next_at"]),
|
|
365
|
+
"createdAt": ms(row["created_at"]), "updatedAt": ms(row["updated_at"]),
|
|
366
|
+
"event": row.get("event") or "run.finished"}
|
|
367
|
+
|
|
368
|
+
def deliveries(self, run_id):
|
|
369
|
+
return [self.public_delivery(row) for row in self.store.deliveries_for(run_id)
|
|
370
|
+
if (row.get("event") or "run.finished") == "run.finished"]
|
|
371
|
+
|
|
372
|
+
def history(self, destination_id, limit=outlet_store.LOG_MAX):
|
|
373
|
+
"""The destination's delivery log, newest first."""
|
|
374
|
+
row = self._get(destination_id)
|
|
375
|
+
return [self.public_delivery(item) for item in self.store.deliveries_to(row["id"], limit)]
|
|
376
|
+
|
|
377
|
+
def enqueue(self, run_id, destination_id, event="run.finished", payload=None):
|
|
378
|
+
"""A delivery of ``run_id``'s result (or, with ``payload``, another event) to the destination; its row."""
|
|
379
|
+
row = self.store.destination(destination_id)
|
|
380
|
+
now = self.clock()
|
|
381
|
+
delivery = {"id": uuid.uuid4().hex[:12], "run_id": run_id, "destination_id": destination_id,
|
|
382
|
+
"destination_name": row["name"] if row else "A removed destination",
|
|
383
|
+
"status": "sending" if row else "failed", "attempts": 0, "http_status": None,
|
|
384
|
+
"error": None if row else REMOVED, "next_at": now if row else None, "created_at": now,
|
|
385
|
+
"updated_at": now, "event": event, "payload": payload}
|
|
386
|
+
saved = self.store.add_delivery(delivery)
|
|
387
|
+
if row is not None:
|
|
388
|
+
self._schedule(saved["id"], now)
|
|
389
|
+
return saved
|
|
390
|
+
|
|
391
|
+
def resend(self, delivery_id):
|
|
392
|
+
"""The same result again, as a new delivery to the same destination."""
|
|
393
|
+
earlier = self.store.delivery(delivery_id) if isinstance(delivery_id, str) else None
|
|
394
|
+
if earlier is None:
|
|
395
|
+
raise APIError("Mobster no longer has that delivery.", 404, "not_found")
|
|
396
|
+
if earlier["status"] == "sending":
|
|
397
|
+
raise APIError("It's still on its way.", 409, "still_sending")
|
|
398
|
+
self._get(earlier["destination_id"])
|
|
399
|
+
if earlier.get("payload") is not None:
|
|
400
|
+
return self.public_delivery(self.enqueue(earlier["run_id"], earlier["destination_id"], earlier["event"],
|
|
401
|
+
earlier["payload"]))
|
|
402
|
+
with self.runtime.lock:
|
|
403
|
+
run = self.runtime.runs.get(earlier["run_id"])
|
|
404
|
+
if run is None or run.finished_at is None:
|
|
405
|
+
raise APIError(GONE_RUN, 409, "run_gone")
|
|
406
|
+
return self.public_delivery(self.enqueue(run.id, earlier["destination_id"]))
|
|
407
|
+
|
|
408
|
+
def _schedule(self, delivery_id, when):
|
|
409
|
+
with self._lock:
|
|
410
|
+
self._due[delivery_id] = when
|
|
411
|
+
self._lock.notify_all()
|
|
412
|
+
self._ensure()
|
|
413
|
+
|
|
414
|
+
def _loop(self):
|
|
415
|
+
while True:
|
|
416
|
+
with self._lock:
|
|
417
|
+
while not self._closed:
|
|
418
|
+
now = self.clock()
|
|
419
|
+
due = [(when, key) for key, when in self._due.items() if when <= now]
|
|
420
|
+
if due:
|
|
421
|
+
break
|
|
422
|
+
wait = min((when - now for when in self._due.values()), default=None)
|
|
423
|
+
self._lock.wait(timeout=1.0 if wait is None else max(0.01, min(wait, 1.0)))
|
|
424
|
+
if self._closed:
|
|
425
|
+
return
|
|
426
|
+
due.sort()
|
|
427
|
+
delivery_id = due[0][1]
|
|
428
|
+
self._due.pop(delivery_id, None)
|
|
429
|
+
try:
|
|
430
|
+
self._attempt(delivery_id)
|
|
431
|
+
except Exception: # noqa: BLE001 -- one bad delivery never stops the others
|
|
432
|
+
log.exception("outlets: a delivery failed unexpectedly")
|
|
433
|
+
try:
|
|
434
|
+
self.store.update_delivery(delivery_id, status="failed", error="Mobster hit an error sending it.",
|
|
435
|
+
next_at=None, updated_at=self.clock())
|
|
436
|
+
except Exception: # noqa: BLE001
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
def _fail(self, delivery_id, message, attempts=None, status=None):
|
|
440
|
+
changes = {"status": "failed", "error": message, "next_at": None, "updated_at": self.clock()}
|
|
441
|
+
if attempts is not None:
|
|
442
|
+
changes.update(attempts=attempts, http_status=status)
|
|
443
|
+
self.store.update_delivery(delivery_id, **changes)
|
|
444
|
+
self._published(delivery_id)
|
|
445
|
+
|
|
446
|
+
def _attempt(self, delivery_id):
|
|
447
|
+
row = self.store.delivery(delivery_id)
|
|
448
|
+
if row is None or row["status"] != "sending":
|
|
449
|
+
return
|
|
450
|
+
destination = self.store.destination(row["destination_id"])
|
|
451
|
+
if destination is None:
|
|
452
|
+
return self._fail(delivery_id, REMOVED)
|
|
453
|
+
try:
|
|
454
|
+
kind = outlet_kinds.get(destination["kind"])
|
|
455
|
+
except ValueError:
|
|
456
|
+
return self._fail(delivery_id, "This Mobster can't send to that kind of destination. Update Mobster.")
|
|
457
|
+
with self._one_try(destination["id"]):
|
|
458
|
+
return self._try(delivery_id, row, destination, kind)
|
|
459
|
+
|
|
460
|
+
def _try(self, delivery_id, row, destination, kind):
|
|
461
|
+
try:
|
|
462
|
+
saved = self._saved(destination)
|
|
463
|
+
except APIError as error:
|
|
464
|
+
return self._fail(delivery_id, str(error))
|
|
465
|
+
event = row.get("event") or "run.finished"
|
|
466
|
+
if row.get("payload") is not None:
|
|
467
|
+
body = row["payload"] # made and masked when the event happened
|
|
468
|
+
else:
|
|
469
|
+
with self.runtime.lock:
|
|
470
|
+
run = self.runtime.runs.get(row["run_id"])
|
|
471
|
+
if run is None:
|
|
472
|
+
return self._fail(delivery_id, GONE_RUN)
|
|
473
|
+
workflow = workflow_of(run)
|
|
474
|
+
screenshot = webhook.screenshot_of(run, getattr(self.runtime, "frames", None)) \
|
|
475
|
+
if destination["screenshot"] and kind.id == "webhook" else None
|
|
476
|
+
body = webhook.payload(run, workflow, screenshot)
|
|
477
|
+
if not kind.replay_safe:
|
|
478
|
+
# Journaled before it is sent: a crash from here on leaves an outcome nobody knows, never a second try.
|
|
479
|
+
self.store.update_delivery(delivery_id, inflight_at=self.clock())
|
|
480
|
+
outcome = kind.send(self._context(destination, saved, delivery_id, event), body)
|
|
481
|
+
attempts = row["attempts"] + 1
|
|
482
|
+
now = self.clock()
|
|
483
|
+
if outcome.ok:
|
|
484
|
+
self.store.update_delivery(delivery_id, status="sent", attempts=attempts, http_status=outcome.status,
|
|
485
|
+
error=None, next_at=None, inflight_at=None, updated_at=now)
|
|
486
|
+
elif outcome.retry and attempts < self.attempts and (kind.replay_safe or outcome.unsent):
|
|
487
|
+
pause = self.backoff[min(attempts - 1, len(self.backoff) - 1)] if self.backoff else 0.0
|
|
488
|
+
pause *= 1 + self.jitter * (2 * self.rand() - 1)
|
|
489
|
+
self.store.update_delivery(delivery_id, attempts=attempts, http_status=outcome.status,
|
|
490
|
+
error=outcome.message, next_at=now + pause, inflight_at=None, updated_at=now)
|
|
491
|
+
self._schedule(delivery_id, now + pause)
|
|
492
|
+
else:
|
|
493
|
+
tried = f" Tried {attempts} times." if attempts > 1 else ""
|
|
494
|
+
self.store.update_delivery(delivery_id, inflight_at=None)
|
|
495
|
+
return self._fail(delivery_id, (outcome.message or "It didn't arrive.") + tried, attempts, outcome.status)
|
|
496
|
+
self._published(delivery_id)
|
|
497
|
+
|
|
498
|
+
def _published(self, delivery_id):
|
|
499
|
+
publish = getattr(self.runtime, "publish", None)
|
|
500
|
+
row = self.store.delivery(delivery_id)
|
|
501
|
+
if callable(publish) and row is not None:
|
|
502
|
+
publish("outlets", {"event": "delivery", "runId": row["run_id"], "deliveryId": row["id"],
|
|
503
|
+
"status": row["status"]})
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def named(run):
|
|
507
|
+
"""The destination id a run's sendTo names, or None."""
|
|
508
|
+
send_to = (getattr(run, "extras", None) or {}).get("sendTo")
|
|
509
|
+
destination = send_to.get("destination") if isinstance(send_to, dict) else None
|
|
510
|
+
return destination if isinstance(destination, str) else None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def workflow_of(run):
|
|
514
|
+
send_to = (getattr(run, "extras", None) or {}).get("sendTo")
|
|
515
|
+
return send_to.get("workflow") if isinstance(send_to, dict) else None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def subscribers(service, run, event):
|
|
519
|
+
"""The destinations that take ``event`` for ``run``: the one it names, when that one takes the event, and each that
|
|
520
|
+
takes it for every run."""
|
|
521
|
+
if harness_api.stays_on_mac(getattr(run, "extras", None)):
|
|
522
|
+
return [] # a result that stays on this Mac goes to no destination
|
|
523
|
+
chosen = named(run)
|
|
524
|
+
out = []
|
|
525
|
+
try:
|
|
526
|
+
rows = service.store.destinations()
|
|
527
|
+
except Exception: # noqa: BLE001 -- a journal that can't be read sends nothing
|
|
528
|
+
return []
|
|
529
|
+
for row in sorted(rows, key=lambda r: r["id"] != chosen):
|
|
530
|
+
config = row.get("config") or {}
|
|
531
|
+
events = config.get("events") or ["run.finished"]
|
|
532
|
+
if event in events and (row["id"] == chosen or config.get("scope") == "all"):
|
|
533
|
+
out.append(row["id"])
|
|
534
|
+
if event == "run.finished" and chosen and chosen not in out:
|
|
535
|
+
out.insert(0, chosen) # a named destination always gets the result (an old row, or a removed one)
|
|
536
|
+
return out
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class Listener:
|
|
540
|
+
"""Deliveries for a run's events, made on the listeners' thread, never the run's: its result to the destination it
|
|
541
|
+
names and to each that takes every run's results; run.started and needs_you to those that take them."""
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def _service(run, runtime=None):
|
|
545
|
+
for service in services():
|
|
546
|
+
if runtime is not None and service.runtime is runtime:
|
|
547
|
+
return service
|
|
548
|
+
runs = getattr(service.runtime, "runs", None) or {}
|
|
549
|
+
if runs.get(run.id) is run:
|
|
550
|
+
return service
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
def on_created(self, run, runtime):
|
|
554
|
+
service = self._service(run, runtime)
|
|
555
|
+
if service is None:
|
|
556
|
+
return
|
|
557
|
+
targets = subscribers(service, run, "run.started")
|
|
558
|
+
if targets:
|
|
559
|
+
payload = webhook.started_payload(run, workflow_of(run))
|
|
560
|
+
for destination in targets:
|
|
561
|
+
service.enqueue(run.id, destination, "run.started", payload)
|
|
562
|
+
|
|
563
|
+
def on_event(self, run, event):
|
|
564
|
+
if not isinstance(event, dict) or event.get("event") != "approval_requested" or event.get("preapproved"):
|
|
565
|
+
return
|
|
566
|
+
service = self._service(run)
|
|
567
|
+
if service is None:
|
|
568
|
+
return
|
|
569
|
+
targets = subscribers(service, run, "needs_you")
|
|
570
|
+
if targets:
|
|
571
|
+
payload = webhook.needs_payload(run, event, workflow_of(run), service.clock())
|
|
572
|
+
for destination in targets:
|
|
573
|
+
service.enqueue(run.id, destination, "needs_you", payload)
|
|
574
|
+
|
|
575
|
+
def on_finished(self, run, summary, private):
|
|
576
|
+
service = self._service(run)
|
|
577
|
+
if service is None:
|
|
578
|
+
return
|
|
579
|
+
for destination in subscribers(service, run, "run.finished"):
|
|
580
|
+
service.enqueue(run.id, destination)
|