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,169 @@
|
|
|
1
|
+
"""A Google Sheets destination: each result is a row in "Mobster results",
|
|
2
|
+
a sheet Mobster makes in your Google Drive."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import quote, urlencode
|
|
7
|
+
|
|
8
|
+
from . import connect, net
|
|
9
|
+
from .kinds import Kind, again, careful, connection, flat
|
|
10
|
+
from .webhook import USER_AGENT, Outcome
|
|
11
|
+
|
|
12
|
+
SHEETS = "https://sheets.googleapis.com/v4/spreadsheets"
|
|
13
|
+
DRIVE = "https://www.googleapis.com/drive/v3/files"
|
|
14
|
+
TITLE = "Mobster results"
|
|
15
|
+
LABEL = "Google Sheets"
|
|
16
|
+
SHEET_ID = re.compile(r"[A-Za-z0-9_-]{20,200}")
|
|
17
|
+
CELL_MAX = 50_000 # Google's limit for one cell's text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def headers(token, body=False):
|
|
21
|
+
out = {"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT}
|
|
22
|
+
if body:
|
|
23
|
+
out["Content-Type"] = "application/json"
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def refusal(status):
|
|
28
|
+
"""Google's refusal as a sentence: what happened, then what to do."""
|
|
29
|
+
if status == 401:
|
|
30
|
+
return f"{LABEL} signed Mobster out. " + again(LABEL)
|
|
31
|
+
if status in (403, 404):
|
|
32
|
+
return (f"Mobster can't open {TITLE} any more: it was deleted, or Google took back Mobster's access. "
|
|
33
|
+
f"Connect {LABEL} again to make a new one.")
|
|
34
|
+
return f"Google answered {status}."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_or_make(call, token):
|
|
38
|
+
"""{"id", "title"}: the "Mobster results" sheet Mobster made before, else a new one. NetError, or ValueError."""
|
|
39
|
+
query = f"name = '{TITLE}' and mimeType = 'application/vnd.google-apps.spreadsheet' and trashed = false"
|
|
40
|
+
reply = call("GET", f"{DRIVE}?" + urlencode({"q": query, "fields": "files(id,name)", "orderBy": "createdTime",
|
|
41
|
+
"pageSize": "1", "spaces": "drive"}), None, headers(token))
|
|
42
|
+
answer = reply.json_object()
|
|
43
|
+
if reply.status != 200 or answer is None:
|
|
44
|
+
raise ValueError(refusal(reply.status))
|
|
45
|
+
files = [item for item in answer.get("files") or () if isinstance(item, dict)
|
|
46
|
+
and isinstance(item.get("id"), str) and SHEET_ID.fullmatch(item["id"])]
|
|
47
|
+
if files:
|
|
48
|
+
return {"id": files[0]["id"], "title": TITLE}
|
|
49
|
+
sheet = {"properties": {"title": TITLE},
|
|
50
|
+
"sheets": [{"properties": {"title": "Results", "gridProperties": {"frozenRowCount": 1}}}]}
|
|
51
|
+
reply = call("POST", SHEETS, json.dumps(sheet).encode(), headers(token, body=True))
|
|
52
|
+
answer = reply.json_object()
|
|
53
|
+
made = (answer or {}).get("spreadsheetId")
|
|
54
|
+
if reply.status != 200 or not isinstance(made, str) or not SHEET_ID.fullmatch(made):
|
|
55
|
+
raise ValueError(f"Google didn't make the sheet ({reply.status}). Connect again.")
|
|
56
|
+
return {"id": made, "title": TITLE}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def value(item):
|
|
60
|
+
"""A cell: numbers and true/false as they are, anything else as text Google keeps as written (RAW)."""
|
|
61
|
+
if item is None:
|
|
62
|
+
return ""
|
|
63
|
+
if isinstance(item, bool) or (isinstance(item, (int, float)) and item == item and abs(item) != float("inf")):
|
|
64
|
+
return item
|
|
65
|
+
text = item if isinstance(item, str) else json.dumps(item, ensure_ascii=False)
|
|
66
|
+
return text[:CELL_MAX]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Stop(Exception):
|
|
70
|
+
"""One try ends here, with ``outcome``."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, outcome):
|
|
73
|
+
super().__init__(outcome.message)
|
|
74
|
+
self.outcome = outcome
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Sheets(Kind):
|
|
78
|
+
id, label = "sheets", LABEL
|
|
79
|
+
example = "Sales sheet"
|
|
80
|
+
fields = ("connect",)
|
|
81
|
+
replay_safe = False
|
|
82
|
+
provider = "google-sheets"
|
|
83
|
+
|
|
84
|
+
def connected(self, call, tokens):
|
|
85
|
+
sheet = find_or_make(call, tokens["access_token"])
|
|
86
|
+
oauth = {key: item for key, item in tokens.items() if key != "kept"}
|
|
87
|
+
return {"credentials": {"oauth": oauth, "sheet": sheet}, "account": sheet["title"], "targets": None}
|
|
88
|
+
|
|
89
|
+
def setup(self, body, current=None):
|
|
90
|
+
if current is not None and "connect" not in body:
|
|
91
|
+
return {}
|
|
92
|
+
found = connection(body, self)["credentials"]
|
|
93
|
+
return {"hint": f"{LABEL} › {found['sheet']['title']}", "has_secret": True,
|
|
94
|
+
"config": {"spreadsheet": found["sheet"]["id"], "title": found["sheet"]["title"]},
|
|
95
|
+
"vault": {"url": None, "secret": None, "oauth": found["oauth"]}, "last_test": None}
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _sheet(ctx):
|
|
99
|
+
sheet = ctx.destination["config"].get("spreadsheet")
|
|
100
|
+
if not sheet:
|
|
101
|
+
# A sheet saved before 15 Oct 2026 took rows through an Apps Script web app.
|
|
102
|
+
raise Stop(Outcome(False, False, None, "Mobster now adds rows itself, with no script. " + again(LABEL),
|
|
103
|
+
unsent=True))
|
|
104
|
+
return f"{SHEETS}/{quote(sheet, safe='')}"
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _call(ctx, method, url, data, token):
|
|
108
|
+
"""One request that adds nothing (a read, or the header, written the same each time it's tried), with a token
|
|
109
|
+
that works: a 401 renews it once."""
|
|
110
|
+
for refresh in (False, True):
|
|
111
|
+
if refresh:
|
|
112
|
+
token = connect.access(ctx, LABEL, refresh=True)
|
|
113
|
+
try:
|
|
114
|
+
reply = ctx.call(method, url, data, headers(token, body=data is not None))
|
|
115
|
+
except net.NetError as error:
|
|
116
|
+
# Nothing this sends can add a row, so another try is safe even when this one may have arrived.
|
|
117
|
+
raise Stop(Outcome(False, error.retry, None, error.message, unsent=True)) from None
|
|
118
|
+
if reply.status == 401 and not refresh:
|
|
119
|
+
continue
|
|
120
|
+
if reply.status == 200:
|
|
121
|
+
return reply, token
|
|
122
|
+
raise Stop(Outcome(False, reply.status == 429 or reply.status >= 500, reply.status,
|
|
123
|
+
refusal(reply.status), unsent=True))
|
|
124
|
+
raise Stop(Outcome(False, False, 401, refusal(401), unsent=True))
|
|
125
|
+
|
|
126
|
+
def _header(self, ctx, base, token):
|
|
127
|
+
reply, token = self._call(ctx, "GET", f"{base}/values/1:1?majorDimension=ROWS", None, token)
|
|
128
|
+
rows = (reply.json_object() or {}).get("values") or [[]]
|
|
129
|
+
return [str(name) for name in rows[0]], token
|
|
130
|
+
|
|
131
|
+
def send(self, ctx, body):
|
|
132
|
+
try:
|
|
133
|
+
base = self._sheet(ctx)
|
|
134
|
+
token = connect.access(ctx, LABEL)
|
|
135
|
+
header, token = self._header(ctx, base, token)
|
|
136
|
+
row = flat(body)
|
|
137
|
+
known = {name.casefold() for name in header}
|
|
138
|
+
missing = [name for name in row if name.casefold() not in known]
|
|
139
|
+
if missing:
|
|
140
|
+
header += missing
|
|
141
|
+
data = json.dumps({"values": [header]}, ensure_ascii=False).encode()
|
|
142
|
+
_reply, token = self._call(ctx, "PUT", f"{base}/values/A1?valueInputOption=RAW", data, token)
|
|
143
|
+
except (Stop, connect.NoToken) as error:
|
|
144
|
+
return error.outcome
|
|
145
|
+
by_name = {name.casefold(): item for name, item in row.items()}
|
|
146
|
+
data = json.dumps({"values": [[value(by_name.get(name.casefold())) for name in header]]},
|
|
147
|
+
ensure_ascii=False).encode()
|
|
148
|
+
# RAW input, so text from a screen is never read as a formula.
|
|
149
|
+
url = f"{base}/values/A1:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS"
|
|
150
|
+
reply, failed = careful(lambda: ctx.call("POST", url, data, headers(token, body=True)), TITLE)
|
|
151
|
+
if failed:
|
|
152
|
+
return failed
|
|
153
|
+
if reply.status == 200:
|
|
154
|
+
return Outcome(True, False, 200)
|
|
155
|
+
if reply.status == 429:
|
|
156
|
+
return Outcome(False, True, 429, "Google asked Mobster to wait.", unsent=True)
|
|
157
|
+
if reply.status >= 500:
|
|
158
|
+
return Outcome(False, False, reply.status, f"Google answered {reply.status}. Check {TITLE}, then resend "
|
|
159
|
+
"if the row is missing.")
|
|
160
|
+
return Outcome(False, False, reply.status, refusal(reply.status), unsent=True)
|
|
161
|
+
|
|
162
|
+
def test(self, ctx):
|
|
163
|
+
"""Reads the header: the sign-in and the sheet work. No row is added."""
|
|
164
|
+
try:
|
|
165
|
+
self._header(ctx, self._sheet(ctx), connect.access(ctx, LABEL))
|
|
166
|
+
except (Stop, connect.NoToken) as error:
|
|
167
|
+
return Outcome(False, False, error.outcome.status, error.outcome.message)
|
|
168
|
+
title = ctx.destination["config"].get("title") or TITLE
|
|
169
|
+
return Outcome(True, False, 200, f"Connected to {title}. Each result adds a row.")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""A Slack destination: each result is a message in one channel you picked, posted as you."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
from ..api_errors import APIError
|
|
8
|
+
from . import webhook
|
|
9
|
+
from .kinds import Kind, again, careful, connection, target
|
|
10
|
+
from .webhook import USER_AGENT, Outcome
|
|
11
|
+
|
|
12
|
+
API = "https://slack.com/api"
|
|
13
|
+
LABEL = "Slack"
|
|
14
|
+
CHANNEL = re.compile(r"[CG][A-Z0-9]{2,30}")
|
|
15
|
+
PAGES = 5 # users.conversations pages of 200 read at Connect
|
|
16
|
+
SAFE_LINK = re.compile(r"https://[A-Za-z0-9._~:/?#\[\]@!$'()*+,;=%&-]{1,500}")
|
|
17
|
+
SECTION = 3000 # Block Kit's limit for a section's text
|
|
18
|
+
STATUS = {"run.started": "Started", "needs_you": "Needs you in Mobster", "test": "Test"}
|
|
19
|
+
RENEW = ("invalid_auth", "token_expired", "not_authed")
|
|
20
|
+
SIGNED_OUT = ("token_revoked", "account_inactive", "user_removed_from_team", "team_disabled")
|
|
21
|
+
NOT_HERE = {"channel_not_found": "the channel is gone", "is_archived": "the channel was archived",
|
|
22
|
+
"not_in_channel": "you're no longer in the channel", "restricted_action": "a workspace admin turned "
|
|
23
|
+
"posting there off", "cant_post": "you can't post in that channel"}
|
|
24
|
+
TEST = "Mobster: this is a test. Results from your tasks will arrive here."
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def headers(token, body=False):
|
|
28
|
+
out = {"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT}
|
|
29
|
+
if body:
|
|
30
|
+
out["Content-Type"] = "application/json; charset=utf-8"
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def refusal(error):
|
|
35
|
+
"""Slack's ``error`` as a sentence: what happened, then what to do."""
|
|
36
|
+
if error in RENEW or error in SIGNED_OUT:
|
|
37
|
+
return "Slack signed Mobster out. Sign in to Slack again under Use in tasks in Settings › Connectors."
|
|
38
|
+
if error == "missing_scope":
|
|
39
|
+
return ("Your Slack sign-in can't do this: it needs chat:write, channels:read and groups:read. Sign in to "
|
|
40
|
+
"Slack again under Use in tasks.")
|
|
41
|
+
if error in NOT_HERE:
|
|
42
|
+
return f"Slack refused: {NOT_HERE[error]}. Pick another channel: " + again(LABEL)
|
|
43
|
+
return f"Slack refused ({str(error)[:60]})."
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read(call, method, token, query=None):
|
|
47
|
+
"""One Web API read: Slack's answer when ``ok``. NetError, or ValueError with Slack's refusal."""
|
|
48
|
+
reply = call("GET", f"{API}/{method}" + (f"?{urlencode(query)}" if query else ""), None, headers(token))
|
|
49
|
+
answer = reply.json_object()
|
|
50
|
+
if reply.status != 200 or answer is None:
|
|
51
|
+
raise ValueError(f"Slack answered {reply.status}. Try again.")
|
|
52
|
+
if answer.get("ok") is not True:
|
|
53
|
+
raise ValueError(refusal(answer.get("error")))
|
|
54
|
+
return answer
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def channels(call, token):
|
|
58
|
+
"""The channels you're in, by name: [{"id", "name"}]."""
|
|
59
|
+
found, cursor = [], None
|
|
60
|
+
for _ in range(PAGES):
|
|
61
|
+
query = {"types": "public_channel,private_channel", "exclude_archived": "true", "limit": "200"}
|
|
62
|
+
if cursor:
|
|
63
|
+
query["cursor"] = cursor
|
|
64
|
+
answer = read(call, "users.conversations", token, query)
|
|
65
|
+
for item in answer.get("channels") or ():
|
|
66
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str) and CHANNEL.fullmatch(item["id"]) \
|
|
67
|
+
and isinstance(item.get("name"), str):
|
|
68
|
+
found.append({"id": item["id"], "name": "#" + item["name"][:80]})
|
|
69
|
+
cursor = (answer.get("response_metadata") or {}).get("next_cursor")
|
|
70
|
+
if not cursor:
|
|
71
|
+
break
|
|
72
|
+
return sorted(found, key=lambda item: item["name"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Slack(Kind):
|
|
76
|
+
id, label = "slack", LABEL
|
|
77
|
+
example = "Team channel"
|
|
78
|
+
fields = ("connect", "target")
|
|
79
|
+
replay_safe = False
|
|
80
|
+
keychain = False
|
|
81
|
+
|
|
82
|
+
def connected(self, call, tokens):
|
|
83
|
+
token = tokens["access_token"]
|
|
84
|
+
team = read(call, "auth.test", token).get("team")
|
|
85
|
+
return {"credentials": {"connector": tokens["connector"]},
|
|
86
|
+
"account": team[:100] if isinstance(team, str) and team.strip() else None,
|
|
87
|
+
"targets": channels(call, token)}
|
|
88
|
+
|
|
89
|
+
def setup(self, body, current=None):
|
|
90
|
+
if current is not None and not ({"connect", "target"} & set(body)):
|
|
91
|
+
return {}
|
|
92
|
+
found = connection(body, self)
|
|
93
|
+
channel = target(found, body.get("target"), "a channel")
|
|
94
|
+
config = {"connector": found["credentials"]["connector"], "channel": channel["id"],
|
|
95
|
+
"channelName": channel["name"]}
|
|
96
|
+
if found.get("account"):
|
|
97
|
+
config["workspace"] = found["account"]
|
|
98
|
+
return {"hint": f"Slack › {channel['name']}", "config": config, "last_test": None}
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def suite_message(body):
|
|
102
|
+
"""A suite's message (testkit/notify.py): the title bold, one section of its lines, and a context line."""
|
|
103
|
+
report = body["report"]
|
|
104
|
+
title = webhook.slack_text(" ".join(str(report["title"]).split()))[:SECTION]
|
|
105
|
+
link = report.get("link")
|
|
106
|
+
safe = isinstance(link, str) and SAFE_LINK.fullmatch(link)
|
|
107
|
+
lines = [f"<{link.replace('&', '&')}|Report>" if safe and line == f"Report: {link}"
|
|
108
|
+
else webhook.slack_text(" ".join(str(line).split())) for line in report["lines"]]
|
|
109
|
+
text = "\n".join(lines)
|
|
110
|
+
budget = SECTION - len(title) - 3 # bold title and newline share this section
|
|
111
|
+
text = text if len(text) <= budget else text[:budget - 1] + "…"
|
|
112
|
+
return {"text": title, "unfurl_links": False, "unfurl_media": False, "link_names": False,
|
|
113
|
+
"blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": f"*{title}*\n{text}"}},
|
|
114
|
+
{"type": "context", "elements": [{"type": "mrkdwn", "text": "Checks run by Mobster"}]}]}
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def message(body):
|
|
118
|
+
"""Block Kit: the line as one section, and how the task ended as a quiet context line. &, < and > are escaped:
|
|
119
|
+
text from a screen can't mention @channel or a person, or make a link."""
|
|
120
|
+
text = webhook.slack_text(" ".join(str(body.get("text") or "Mobster").split()))
|
|
121
|
+
text = text if len(text) <= SECTION else text[:SECTION - 1] + "…"
|
|
122
|
+
kind = body.get("type")
|
|
123
|
+
if kind == "tests.finished":
|
|
124
|
+
return Slack.suite_message(body)
|
|
125
|
+
status = STATUS.get(kind) or ("Done" if body.get("status") in webhook.DONE else "Couldn't finish")
|
|
126
|
+
flow = (body.get("workflow") or {}).get("name")
|
|
127
|
+
context = webhook.slack_text(f"{status} in Mobster" + (f", from {flow}" if flow else ""))
|
|
128
|
+
return {"text": text, "unfurl_links": False, "unfurl_media": False, "link_names": False,
|
|
129
|
+
"blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": text}},
|
|
130
|
+
{"type": "context", "elements": [{"type": "mrkdwn", "text": context}]}]}
|
|
131
|
+
|
|
132
|
+
def _post(self, ctx, message):
|
|
133
|
+
config = ctx.destination["config"]
|
|
134
|
+
if not config.get("channel") or not config.get("connector"):
|
|
135
|
+
# A Slack destination saved before 15 Oct 2026 posted through an incoming webhook.
|
|
136
|
+
return Outcome(False, False, None, "Mobster now posts as you, in a channel you pick. " + again(LABEL),
|
|
137
|
+
unsent=True)
|
|
138
|
+
connectors = ctx.connectors()
|
|
139
|
+
if connectors is None:
|
|
140
|
+
return Outcome(False, True, None, "Mobster's connectors aren't running. Restart Mobster, then resend.",
|
|
141
|
+
unsent=True)
|
|
142
|
+
from ..connectors import client as mcp
|
|
143
|
+
data = json.dumps({**message, "channel": config["channel"]}, ensure_ascii=False).encode()
|
|
144
|
+
for renew in (False, True):
|
|
145
|
+
try:
|
|
146
|
+
token = connectors.bearer(config["connector"], refresh=renew)
|
|
147
|
+
except APIError:
|
|
148
|
+
return Outcome(False, False, None, "Slack was disconnected from Mobster. Connect Slack under Use in "
|
|
149
|
+
"tasks, then connect this destination again.", unsent=True)
|
|
150
|
+
except mcp.AuthNeeded:
|
|
151
|
+
return Outcome(False, False, None, refusal("token_revoked"), unsent=True)
|
|
152
|
+
except mcp.NotSent as error:
|
|
153
|
+
return Outcome(False, True, None, error.message, unsent=True)
|
|
154
|
+
reply, failed = careful(lambda: ctx.call("POST", f"{API}/chat.postMessage", data,
|
|
155
|
+
headers(token, body=True)), "the channel")
|
|
156
|
+
if failed:
|
|
157
|
+
return failed
|
|
158
|
+
if reply.status == 429:
|
|
159
|
+
return Outcome(False, True, 429, "Slack asked Mobster to wait.", unsent=True)
|
|
160
|
+
if reply.status >= 500:
|
|
161
|
+
return Outcome(False, False, reply.status, f"Slack answered {reply.status}. Check the channel, then "
|
|
162
|
+
"resend if the message is missing.")
|
|
163
|
+
answer = reply.json_object() or {}
|
|
164
|
+
if reply.status == 200 and answer.get("ok") is True:
|
|
165
|
+
return Outcome(True, False, 200)
|
|
166
|
+
error = answer.get("error")
|
|
167
|
+
if error in RENEW and not renew:
|
|
168
|
+
continue
|
|
169
|
+
# Slack answered ok: false, so it posted nothing.
|
|
170
|
+
return Outcome(False, error == "ratelimited", reply.status, refusal(error), unsent=True)
|
|
171
|
+
return Outcome(False, False, 401, refusal("invalid_auth"), unsent=True)
|
|
172
|
+
|
|
173
|
+
def send(self, ctx, body):
|
|
174
|
+
return self._post(ctx, self.message(body))
|
|
175
|
+
|
|
176
|
+
def test(self, ctx):
|
|
177
|
+
return self._post(ctx, self.message({"type": "test", "text": TEST}))
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Outlets' state in the run journal: schema ``outlets`` v2, in the same private SQLite file (0600)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
|
|
6
|
+
from ..journal import JournalError
|
|
7
|
+
|
|
8
|
+
SCHEMA = "outlets"
|
|
9
|
+
VERSION = 3
|
|
10
|
+
MAX_DESTINATIONS = 50
|
|
11
|
+
# Deliveries kept: the newest, for the runs History still has.
|
|
12
|
+
MAX_DELIVERIES = 2000
|
|
13
|
+
STATUSES = ("sending", "sent", "failed")
|
|
14
|
+
EVENTS = ("run.finished", "run.started", "needs_you")
|
|
15
|
+
LOG_MAX = 50 # deliveries one destination's log shows
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def migrate(connection, from_version):
|
|
19
|
+
"""journal.register_schema's migration: v0 -> v1 creates the tables; v1 -> v2 adds the kinds."""
|
|
20
|
+
if from_version < 1:
|
|
21
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS outlet_destinations (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
name TEXT NOT NULL,
|
|
24
|
+
hint TEXT NOT NULL,
|
|
25
|
+
has_secret INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
screenshot INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
last_test TEXT,
|
|
28
|
+
created_at REAL NOT NULL,
|
|
29
|
+
updated_at REAL NOT NULL)""")
|
|
30
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS outlet_deliveries (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
run_id TEXT NOT NULL,
|
|
33
|
+
destination_id TEXT NOT NULL,
|
|
34
|
+
destination_name TEXT NOT NULL,
|
|
35
|
+
status TEXT NOT NULL CHECK (status IN ('sending', 'sent', 'failed')),
|
|
36
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
http_status INTEGER,
|
|
38
|
+
error TEXT,
|
|
39
|
+
next_at REAL,
|
|
40
|
+
created_at REAL NOT NULL,
|
|
41
|
+
updated_at REAL NOT NULL)""")
|
|
42
|
+
connection.execute("CREATE INDEX IF NOT EXISTS outlet_deliveries_run ON outlet_deliveries(run_id)")
|
|
43
|
+
if from_version < 2:
|
|
44
|
+
columns = {row[1] for row in connection.execute("PRAGMA table_info(outlet_destinations)")}
|
|
45
|
+
if "kind" not in columns:
|
|
46
|
+
connection.execute("ALTER TABLE outlet_destinations ADD COLUMN kind TEXT NOT NULL DEFAULT 'webhook'")
|
|
47
|
+
if "config" not in columns:
|
|
48
|
+
connection.execute("ALTER TABLE outlet_destinations ADD COLUMN config TEXT")
|
|
49
|
+
if "inflight_at" not in {row[1] for row in connection.execute("PRAGMA table_info(outlet_deliveries)")}:
|
|
50
|
+
connection.execute("ALTER TABLE outlet_deliveries ADD COLUMN inflight_at REAL")
|
|
51
|
+
if from_version < 3:
|
|
52
|
+
columns = {row[1] for row in connection.execute("PRAGMA table_info(outlet_deliveries)")}
|
|
53
|
+
if "event" not in columns:
|
|
54
|
+
connection.execute("ALTER TABLE outlet_deliveries ADD COLUMN event TEXT NOT NULL DEFAULT 'run.finished'")
|
|
55
|
+
if "payload" not in columns:
|
|
56
|
+
connection.execute("ALTER TABLE outlet_deliveries ADD COLUMN payload TEXT")
|
|
57
|
+
connection.execute("CREATE INDEX IF NOT EXISTS outlet_deliveries_destination "
|
|
58
|
+
"ON outlet_deliveries(destination_id, created_at)")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
DESTINATION_COLUMNS = ("id", "name", "hint", "has_secret", "screenshot", "last_test", "created_at", "updated_at",
|
|
62
|
+
"kind", "config")
|
|
63
|
+
DELIVERY_COLUMNS = ("id", "run_id", "destination_id", "destination_name", "status", "attempts", "http_status", "error",
|
|
64
|
+
"next_at", "created_at", "updated_at", "inflight_at", "event", "payload")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _destination(row):
|
|
68
|
+
if row is None:
|
|
69
|
+
return None
|
|
70
|
+
value = dict(zip(DESTINATION_COLUMNS, row))
|
|
71
|
+
value["has_secret"], value["screenshot"] = bool(value["has_secret"]), bool(value["screenshot"])
|
|
72
|
+
for key in ("last_test", "config"):
|
|
73
|
+
try:
|
|
74
|
+
value[key] = json.loads(value[key]) if value[key] else None
|
|
75
|
+
except ValueError:
|
|
76
|
+
value[key] = None
|
|
77
|
+
value["kind"] = value["kind"] or "webhook"
|
|
78
|
+
value["config"] = value["config"] if isinstance(value["config"], dict) else {}
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _delivery(row):
|
|
83
|
+
if row is None:
|
|
84
|
+
return None
|
|
85
|
+
value = dict(zip(DELIVERY_COLUMNS, row))
|
|
86
|
+
value["event"] = value["event"] or "run.finished"
|
|
87
|
+
try:
|
|
88
|
+
value["payload"] = json.loads(value["payload"]) if value["payload"] else None
|
|
89
|
+
except ValueError:
|
|
90
|
+
value["payload"] = None
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OutletStore:
|
|
95
|
+
def __init__(self, journal):
|
|
96
|
+
self.journal = journal
|
|
97
|
+
|
|
98
|
+
def _read(self, sql, args=()):
|
|
99
|
+
with self.journal.lock:
|
|
100
|
+
if self.journal.connection is None:
|
|
101
|
+
raise JournalError("The run journal is closed")
|
|
102
|
+
try:
|
|
103
|
+
return self.journal.connection.execute(sql, args).fetchall()
|
|
104
|
+
except sqlite3.Error:
|
|
105
|
+
raise JournalError("Mobster could not read its saved destinations") from None
|
|
106
|
+
|
|
107
|
+
def destinations(self):
|
|
108
|
+
cols = ", ".join(DESTINATION_COLUMNS)
|
|
109
|
+
return [_destination(row) for row in self._read(f"SELECT {cols} FROM outlet_destinations "
|
|
110
|
+
"ORDER BY created_at, rowid")]
|
|
111
|
+
|
|
112
|
+
def destination(self, destination_id):
|
|
113
|
+
cols = ", ".join(DESTINATION_COLUMNS)
|
|
114
|
+
rows = self._read(f"SELECT {cols} FROM outlet_destinations WHERE id=?", (destination_id,))
|
|
115
|
+
return _destination(rows[0]) if rows else None
|
|
116
|
+
|
|
117
|
+
def count(self):
|
|
118
|
+
return self._read("SELECT count(*) FROM outlet_destinations")[0][0]
|
|
119
|
+
|
|
120
|
+
def add_destination(self, row):
|
|
121
|
+
with self.journal.transaction() as connection:
|
|
122
|
+
connection.execute(f"INSERT INTO outlet_destinations({', '.join(DESTINATION_COLUMNS)}) "
|
|
123
|
+
f"VALUES ({', '.join('?' * len(DESTINATION_COLUMNS))})",
|
|
124
|
+
[row["id"], row["name"], row["hint"], int(bool(row["has_secret"])),
|
|
125
|
+
int(bool(row["screenshot"])), None, row["created_at"], row["updated_at"],
|
|
126
|
+
row.get("kind") or "webhook", json.dumps(row.get("config") or {})])
|
|
127
|
+
return self.destination(row["id"])
|
|
128
|
+
|
|
129
|
+
def update_destination(self, destination_id, **changes):
|
|
130
|
+
allowed = {"name", "hint", "has_secret", "screenshot", "last_test", "updated_at", "config"}
|
|
131
|
+
if set(changes) - allowed:
|
|
132
|
+
raise ValueError("Unknown destination field")
|
|
133
|
+
values = []
|
|
134
|
+
for key, value in changes.items():
|
|
135
|
+
if key in ("has_secret", "screenshot"):
|
|
136
|
+
value = int(bool(value))
|
|
137
|
+
elif key in ("last_test", "config"):
|
|
138
|
+
value = json.dumps(value) if value is not None else None
|
|
139
|
+
values.append(value)
|
|
140
|
+
if values:
|
|
141
|
+
with self.journal.transaction() as connection:
|
|
142
|
+
connection.execute(f"UPDATE outlet_destinations SET {', '.join(f'{k}=?' for k in changes)} WHERE id=?",
|
|
143
|
+
[*values, destination_id])
|
|
144
|
+
return self.destination(destination_id)
|
|
145
|
+
|
|
146
|
+
def delete_destination(self, destination_id):
|
|
147
|
+
with self.journal.transaction() as connection:
|
|
148
|
+
connection.execute("DELETE FROM outlet_destinations WHERE id=?", (destination_id,))
|
|
149
|
+
|
|
150
|
+
def add_delivery(self, row):
|
|
151
|
+
with self.journal.transaction() as connection:
|
|
152
|
+
connection.execute(f"INSERT INTO outlet_deliveries({', '.join(DELIVERY_COLUMNS)}) "
|
|
153
|
+
f"VALUES ({', '.join('?' * len(DELIVERY_COLUMNS))})",
|
|
154
|
+
[json.dumps(row["payload"], ensure_ascii=False) if column == "payload" and
|
|
155
|
+
row.get("payload") is not None else
|
|
156
|
+
(row.get(column) or "run.finished") if column == "event" else row.get(column)
|
|
157
|
+
for column in DELIVERY_COLUMNS])
|
|
158
|
+
excess = connection.execute("SELECT count(*) FROM outlet_deliveries").fetchone()[0] - MAX_DELIVERIES
|
|
159
|
+
if excess > 0:
|
|
160
|
+
connection.execute("DELETE FROM outlet_deliveries WHERE rowid IN (SELECT rowid FROM outlet_deliveries "
|
|
161
|
+
"WHERE status != 'sending' ORDER BY created_at LIMIT ?)", (excess,))
|
|
162
|
+
return self.delivery(row["id"])
|
|
163
|
+
|
|
164
|
+
def delivery(self, delivery_id):
|
|
165
|
+
rows = self._read(f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE id=?", (delivery_id,))
|
|
166
|
+
return _delivery(rows[0]) if rows else None
|
|
167
|
+
|
|
168
|
+
def deliveries_for(self, run_id):
|
|
169
|
+
return [_delivery(row) for row in self._read(
|
|
170
|
+
f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE run_id=? ORDER BY created_at, rowid",
|
|
171
|
+
(run_id,))]
|
|
172
|
+
|
|
173
|
+
def deliveries_to(self, destination_id, limit=LOG_MAX):
|
|
174
|
+
"""A destination's newest deliveries first: its delivery log."""
|
|
175
|
+
return [_delivery(row) for row in self._read(
|
|
176
|
+
f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE destination_id=? "
|
|
177
|
+
"ORDER BY created_at DESC, rowid DESC LIMIT ?", (destination_id, max(1, min(int(limit), LOG_MAX))))]
|
|
178
|
+
|
|
179
|
+
def pending(self):
|
|
180
|
+
return [_delivery(row) for row in self._read(
|
|
181
|
+
f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE status='sending' ORDER BY next_at")]
|
|
182
|
+
|
|
183
|
+
def update_delivery(self, delivery_id, **changes):
|
|
184
|
+
allowed = {"status", "attempts", "http_status", "error", "next_at", "updated_at", "inflight_at"}
|
|
185
|
+
if set(changes) - allowed:
|
|
186
|
+
raise ValueError("Unknown delivery field")
|
|
187
|
+
with self.journal.transaction() as connection:
|
|
188
|
+
connection.execute(f"UPDATE outlet_deliveries SET {', '.join(f'{k}=?' for k in changes)} WHERE id=?",
|
|
189
|
+
[*changes.values(), delivery_id])
|
|
190
|
+
return self.delivery(delivery_id)
|
|
191
|
+
|
|
192
|
+
def forget_run(self, run_id):
|
|
193
|
+
with self.journal.transaction() as connection:
|
|
194
|
+
connection.execute("DELETE FROM outlet_deliveries WHERE run_id=?", (run_id,))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Where a destination's address, secret and token live: one generic password per destination in the login Keychain,
|
|
2
|
+
readable only by Mobster's own programs (keychain_acl.py), as remote/keychain.py keeps Mobster Remote's device key."""
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from ..connectors import keystore
|
|
9
|
+
|
|
10
|
+
SERVICE = "app.mobster.desktop.outlets"
|
|
11
|
+
ENV = "MOBSTER_OUTLETS_KEYCHAIN"
|
|
12
|
+
ACCOUNT = re.compile(r"destination:[0-9a-f]{12}")
|
|
13
|
+
WORD = re.compile(r"[A-Za-z0-9_-]{1,8000}")
|
|
14
|
+
VaultError = keystore.VaultError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def account_for(destination_id):
|
|
18
|
+
account = f"destination:{destination_id}"
|
|
19
|
+
if not ACCOUNT.fullmatch(account):
|
|
20
|
+
raise ValueError("A destination's id is 12 hex characters")
|
|
21
|
+
return account
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
EXTRA = {"token": str, "to": str, "oauth": dict}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def encode(url, secret, extra=None):
|
|
28
|
+
item = {"url": url, "secret": secret or None}
|
|
29
|
+
for key, kind in EXTRA.items():
|
|
30
|
+
if isinstance((extra or {}).get(key), kind) and extra[key]:
|
|
31
|
+
item[key] = extra[key]
|
|
32
|
+
raw = json.dumps(item, separators=(",", ":")).encode()
|
|
33
|
+
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decode(word):
|
|
37
|
+
"""{"url", "secret", "token"?, "to"?, "oauth"?}: ``url`` and ``secret`` may be None (a Notion, Sheets or email
|
|
38
|
+
destination)."""
|
|
39
|
+
try:
|
|
40
|
+
raw = base64.urlsafe_b64decode(word + "=" * (-len(word) % 4))
|
|
41
|
+
value = json.loads(raw)
|
|
42
|
+
except (ValueError, TypeError):
|
|
43
|
+
raise VaultError("Mobster couldn't read a saved destination") from None
|
|
44
|
+
kinds = {"url": str, **EXTRA}
|
|
45
|
+
if not isinstance(value, dict) or not any(isinstance(value.get(key), kind) and value[key]
|
|
46
|
+
for key, kind in kinds.items()):
|
|
47
|
+
raise VaultError("Mobster couldn't read a saved destination")
|
|
48
|
+
out = {"url": None, "secret": None}
|
|
49
|
+
for key, kind in {"url": str, "secret": str, **EXTRA}.items():
|
|
50
|
+
found = value.get(key)
|
|
51
|
+
if isinstance(found, kind) and found:
|
|
52
|
+
out[key] = found
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Vault:
|
|
57
|
+
"""``put(id, url, secret, extra)``, ``get(id) -> {"url", "secret", ...} | None`` and ``delete(id)`` over
|
|
58
|
+
a raw store."""
|
|
59
|
+
|
|
60
|
+
def put(self, destination_id, url, secret, extra=None):
|
|
61
|
+
self._set(account_for(destination_id), encode(url, secret, extra))
|
|
62
|
+
|
|
63
|
+
def get(self, destination_id):
|
|
64
|
+
word = self._get(account_for(destination_id))
|
|
65
|
+
return decode(word) if word else None
|
|
66
|
+
|
|
67
|
+
def delete(self, destination_id):
|
|
68
|
+
self._delete(account_for(destination_id))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FileVault(keystore.FileStore, Vault):
|
|
72
|
+
files = "destinations"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class KeychainVault(keystore.KeychainStore, Vault):
|
|
76
|
+
service, noun, word = SERVICE, "destination", WORD
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def backend():
|
|
80
|
+
return keystore.backend(ENV, FileVault, KeychainVault)
|