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,453 @@
|
|
|
1
|
+
"""Incoming webhooks: another tool starts one of your workflows with a signed POST to a URL Mobster Remote serves. The
|
|
2
|
+
relay never holds a hook's secret and never keeps a body: this Mac checks everything."""
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
import sqlite3
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from .. import secret_filter
|
|
15
|
+
from ..api_errors import APIError
|
|
16
|
+
from ..journal import JournalError
|
|
17
|
+
from ..service_support import ServiceRegistry
|
|
18
|
+
from . import vault as outlet_vault
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger("mobster.outlets.hooks")
|
|
21
|
+
|
|
22
|
+
SCHEMA = "outlet_hooks"
|
|
23
|
+
VERSION = 2
|
|
24
|
+
HOOK_ID = re.compile(r"[0-9a-f]{32}")
|
|
25
|
+
WORKFLOW_ID = re.compile(r"[0-9a-f]{12}")
|
|
26
|
+
SIGNATURE = re.compile(r"t=(\d{1,12}),v1=([0-9a-f]{64})")
|
|
27
|
+
WINDOW = 300.0 # seconds a signed call stays good either side of this Mac's clock
|
|
28
|
+
SEEN_KEEP = 2 * WINDOW # signatures kept to refuse a replay
|
|
29
|
+
MAX_HOOKS = 50
|
|
30
|
+
MAX_BODY = 64 * 1024
|
|
31
|
+
MAX_EVENTS = 100_000
|
|
32
|
+
VALUE_CHARS = 300
|
|
33
|
+
EVENT_ID = re.compile(r"[A-Za-z0-9_.:-]{1,128}")
|
|
34
|
+
RATE, RATE_SECONDS = 10, 60.0
|
|
35
|
+
REMOTE_OFF = "Turn on Mobster Remote first, in Settings › Remote. A webhook URL is Remote's address for this Mac."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def migrate(connection, from_version):
|
|
39
|
+
if from_version < 1:
|
|
40
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS outlet_hooks (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
workflow_id TEXT NOT NULL,
|
|
43
|
+
url TEXT NOT NULL,
|
|
44
|
+
created_at REAL NOT NULL,
|
|
45
|
+
last_at REAL,
|
|
46
|
+
last_status INTEGER,
|
|
47
|
+
last_message TEXT,
|
|
48
|
+
uses INTEGER NOT NULL DEFAULT 0)""")
|
|
49
|
+
connection.execute("CREATE INDEX IF NOT EXISTS outlet_hooks_workflow ON outlet_hooks(workflow_id)")
|
|
50
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS outlet_hook_seen (
|
|
51
|
+
hook_id TEXT NOT NULL,
|
|
52
|
+
signature TEXT NOT NULL,
|
|
53
|
+
at REAL NOT NULL,
|
|
54
|
+
PRIMARY KEY (hook_id, signature))""")
|
|
55
|
+
if from_version < 2:
|
|
56
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS outlet_hook_events (
|
|
57
|
+
hook_id TEXT NOT NULL,
|
|
58
|
+
event_id TEXT NOT NULL,
|
|
59
|
+
digest TEXT NOT NULL,
|
|
60
|
+
claimed_at REAL NOT NULL,
|
|
61
|
+
PRIMARY KEY (hook_id, event_id))""")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
COLUMNS = ("id", "workflow_id", "url", "created_at", "last_at", "last_status", "last_message", "uses")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HookStore:
|
|
68
|
+
def __init__(self, journal):
|
|
69
|
+
self.journal = journal
|
|
70
|
+
|
|
71
|
+
def _read(self, sql, args=()):
|
|
72
|
+
with self.journal.lock:
|
|
73
|
+
if self.journal.connection is None:
|
|
74
|
+
raise JournalError("The run journal is closed")
|
|
75
|
+
try:
|
|
76
|
+
return self.journal.connection.execute(sql, args).fetchall()
|
|
77
|
+
except sqlite3.Error:
|
|
78
|
+
raise JournalError("Mobster could not read its webhooks") from None
|
|
79
|
+
|
|
80
|
+
def all(self, workflow_id=None):
|
|
81
|
+
sql = f"SELECT {', '.join(COLUMNS)} FROM outlet_hooks"
|
|
82
|
+
rows = self._read(sql + " WHERE workflow_id=? ORDER BY created_at" if workflow_id else sql
|
|
83
|
+
+ " ORDER BY created_at",
|
|
84
|
+
(workflow_id,) if workflow_id else ())
|
|
85
|
+
return [dict(zip(COLUMNS, row)) for row in rows]
|
|
86
|
+
|
|
87
|
+
def get(self, hook_id):
|
|
88
|
+
rows = self._read(f"SELECT {', '.join(COLUMNS)} FROM outlet_hooks WHERE id=?", (hook_id,))
|
|
89
|
+
return dict(zip(COLUMNS, rows[0])) if rows else None
|
|
90
|
+
|
|
91
|
+
def add(self, row):
|
|
92
|
+
with self.journal.transaction() as connection:
|
|
93
|
+
connection.execute(f"INSERT INTO outlet_hooks({', '.join(COLUMNS)}) "
|
|
94
|
+
f"VALUES ({', '.join('?' * len(COLUMNS))})",
|
|
95
|
+
[row.get(column) for column in COLUMNS])
|
|
96
|
+
|
|
97
|
+
def used(self, hook_id, at, status, message):
|
|
98
|
+
with self.journal.transaction() as connection:
|
|
99
|
+
connection.execute("UPDATE outlet_hooks SET last_at=?, last_status=?, last_message=?, "
|
|
100
|
+
"uses=uses+? WHERE id=?",
|
|
101
|
+
(at, status, message, 1 if status == 202 else 0, hook_id))
|
|
102
|
+
|
|
103
|
+
def delete(self, hook_id):
|
|
104
|
+
with self.journal.transaction() as connection:
|
|
105
|
+
connection.execute("DELETE FROM outlet_hooks WHERE id=?", (hook_id,))
|
|
106
|
+
connection.execute("DELETE FROM outlet_hook_seen WHERE hook_id=?", (hook_id,))
|
|
107
|
+
connection.execute("DELETE FROM outlet_hook_events WHERE hook_id=?", (hook_id,))
|
|
108
|
+
|
|
109
|
+
def claim_event(self, hook_id, event_id, digest, now):
|
|
110
|
+
"""Keep an event's identity until its hook is revoked, including a start whose outcome is unknown."""
|
|
111
|
+
with self.journal.transaction() as connection:
|
|
112
|
+
if connection.execute("SELECT 1 FROM outlet_hooks WHERE id=?", (hook_id,)).fetchone() is None:
|
|
113
|
+
return "gone"
|
|
114
|
+
row = connection.execute("SELECT digest FROM outlet_hook_events WHERE hook_id=? AND event_id=?",
|
|
115
|
+
(hook_id, event_id)).fetchone()
|
|
116
|
+
if row:
|
|
117
|
+
return "replayed" if row[0] == digest else "changed"
|
|
118
|
+
if connection.execute("SELECT count(*) FROM outlet_hook_events").fetchone()[0] >= MAX_EVENTS:
|
|
119
|
+
return "full"
|
|
120
|
+
connection.execute("INSERT INTO outlet_hook_events VALUES (?,?,?,?)", (hook_id, event_id, digest, now))
|
|
121
|
+
return "claimed"
|
|
122
|
+
|
|
123
|
+
def seen(self, hook_id, signature, now):
|
|
124
|
+
"""True when this signature came before (a replay); else remember it. Old ones go."""
|
|
125
|
+
with self.journal.transaction() as connection:
|
|
126
|
+
connection.execute("DELETE FROM outlet_hook_seen WHERE at < ?", (now - SEEN_KEEP,))
|
|
127
|
+
try:
|
|
128
|
+
connection.execute("INSERT INTO outlet_hook_seen(hook_id, signature, at) VALUES (?, ?, ?)",
|
|
129
|
+
(hook_id, signature, now))
|
|
130
|
+
except sqlite3.IntegrityError:
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class HookVault:
|
|
136
|
+
"""A hook's secret in the outlets' Keychain service, account ``hook:<id>``."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, backend=None):
|
|
139
|
+
self.backend = backend if backend is not None else outlet_vault.backend()
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def account(hook_id):
|
|
143
|
+
if not HOOK_ID.fullmatch(str(hook_id)):
|
|
144
|
+
raise ValueError("A webhook's id is 32 hex characters")
|
|
145
|
+
return f"hook:{hook_id}"
|
|
146
|
+
|
|
147
|
+
def put(self, hook_id, secret):
|
|
148
|
+
self.backend._set(self.account(hook_id), secret)
|
|
149
|
+
|
|
150
|
+
def get(self, hook_id):
|
|
151
|
+
return self.backend._get(self.account(hook_id))
|
|
152
|
+
|
|
153
|
+
def delete(self, hook_id):
|
|
154
|
+
self.backend._delete(self.account(hook_id))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def new_secret():
|
|
158
|
+
return "whsec_" + secrets.token_hex(32)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def sign(secret, timestamp, body):
|
|
162
|
+
"""``t=<timestamp>,v1=<hex>`` for ``body`` (bytes or text): what a caller sends."""
|
|
163
|
+
data = body.encode() if isinstance(body, str) else body
|
|
164
|
+
digest = hmac.new(secret.encode(), str(int(timestamp)).encode() + b"." + data, hashlib.sha256).hexdigest()
|
|
165
|
+
return f"t={int(timestamp)},v1={digest}"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def fill(workflow, body):
|
|
169
|
+
"""(values for the workflow's blanks, the field names that named none) from a call's JSON object."""
|
|
170
|
+
from ..workflows import starters
|
|
171
|
+
fields = body.get("blanks") if isinstance(body.get("blanks"), dict) else body
|
|
172
|
+
names = {starters._key(blank["name"]): blank["name"] for blank in starters.blanks(workflow.get("goal"))}
|
|
173
|
+
values, ignored = {}, []
|
|
174
|
+
for key, value in list(fields.items())[:100]:
|
|
175
|
+
name = names.get(starters._key(str(key))) if isinstance(key, str) else None
|
|
176
|
+
if name is None or isinstance(value, (dict, list)) or value is None:
|
|
177
|
+
ignored.append(str(key)[:40])
|
|
178
|
+
continue
|
|
179
|
+
if isinstance(value, bool):
|
|
180
|
+
value = "yes" if value else "no"
|
|
181
|
+
text = " ".join(str(value).split())[:VALUE_CHARS]
|
|
182
|
+
values[name] = secret_filter.redact(text)
|
|
183
|
+
return values, ignored
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def event_envelope(data, workflow, workflows):
|
|
187
|
+
"""A producer's stable event identity and typed values, only for the explicit versioned envelope."""
|
|
188
|
+
if "_mobsterEvent" not in data:
|
|
189
|
+
return None
|
|
190
|
+
from ..output_contract import bounded_json
|
|
191
|
+
from ..workflows import starters
|
|
192
|
+
meta = data["_mobsterEvent"]
|
|
193
|
+
if (set(data) != {"_mobsterEvent", "values"} or not isinstance(meta, dict)
|
|
194
|
+
or set(meta) != {"version", "id", "occurredAt"} or type(meta["version"]) is not int
|
|
195
|
+
or meta["version"] != 1 or not isinstance(meta["id"], str) or not EVENT_ID.fullmatch(meta["id"])
|
|
196
|
+
or type(meta["occurredAt"]) is not int or not 0 <= meta["occurredAt"] <= 9_999_999_999_999
|
|
197
|
+
or not isinstance(data["values"], dict)
|
|
198
|
+
or len(data["values"]) > 30):
|
|
199
|
+
raise ValueError("An event has _mobsterEvent {version: 1, id, occurredAt} and a values object")
|
|
200
|
+
cleaned = bounded_json(data, max_bytes=16_000)
|
|
201
|
+
names = {starters._key(item["name"]): item["name"] for item in workflows.live(workflow).get("inputs") or []}
|
|
202
|
+
names.update({starters._key(blank["name"]): blank["name"] for blank in starters.blanks(workflow.get("goal"))})
|
|
203
|
+
values = {}
|
|
204
|
+
for name, value in cleaned["values"].items():
|
|
205
|
+
if not isinstance(name, str) or len(name) > starters.NAME_CHARS or starters._key(name) not in names:
|
|
206
|
+
raise ValueError(f"{str(name)[:40]!r} is not a declared workflow input")
|
|
207
|
+
key = names[starters._key(name)]
|
|
208
|
+
if key in values:
|
|
209
|
+
raise ValueError(f"Give {key} once in event values")
|
|
210
|
+
values[key] = secret_filter.redact_deep(value)
|
|
211
|
+
digest = hashlib.sha256(json.dumps(cleaned, sort_keys=True, separators=(",", ":"),
|
|
212
|
+
ensure_ascii=False, allow_nan=False).encode()).hexdigest()
|
|
213
|
+
return meta["id"], values, digest
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class HookService:
|
|
217
|
+
"""Make, list and revoke a workflow's hooks, and take a call. One per Runtime (outlets.register)."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, runtime, *, vault=None, clock=time.time, remote=None):
|
|
220
|
+
self.runtime = runtime
|
|
221
|
+
self.store = HookStore(runtime.journal)
|
|
222
|
+
self.vault = vault if vault is not None else HookVault()
|
|
223
|
+
self.clock = clock
|
|
224
|
+
self._remote = remote # tests: an object with address(prefix) and hooks_changed(ids)
|
|
225
|
+
self._buckets = {}
|
|
226
|
+
self._lock = threading.Lock()
|
|
227
|
+
|
|
228
|
+
def remote(self):
|
|
229
|
+
if self._remote is not None:
|
|
230
|
+
return self._remote
|
|
231
|
+
try:
|
|
232
|
+
from ..remote import service as remote_service
|
|
233
|
+
except ImportError:
|
|
234
|
+
return None
|
|
235
|
+
return remote_service.service_for(self.runtime)
|
|
236
|
+
|
|
237
|
+
def base_url(self):
|
|
238
|
+
"""``<relay>/hooks/<route>``, or None when Remote isn't set up on this Mac."""
|
|
239
|
+
remote = self.remote()
|
|
240
|
+
try:
|
|
241
|
+
return remote.address("hooks") if remote is not None else None
|
|
242
|
+
except Exception: # noqa: BLE001
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def ids(self):
|
|
246
|
+
try:
|
|
247
|
+
return [row["id"] for row in self.store.all()]
|
|
248
|
+
except JournalError:
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
def _tell_remote(self):
|
|
252
|
+
remote = self.remote()
|
|
253
|
+
if remote is not None:
|
|
254
|
+
try:
|
|
255
|
+
remote.hooks_changed(self.ids())
|
|
256
|
+
except Exception: # noqa: BLE001 -- the next hello carries the list
|
|
257
|
+
log.warning("hooks: couldn't tell Remote about a change")
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def public(row, secret=None):
|
|
261
|
+
out = {"id": row["id"], "workflowId": row["workflow_id"], "url": row["url"],
|
|
262
|
+
"createdAt": round(row["created_at"] * 1000), "uses": row["uses"],
|
|
263
|
+
"lastAt": round(row["last_at"] * 1000) if row["last_at"] else None, "lastStatus": row["last_status"],
|
|
264
|
+
"lastMessage": row["last_message"]}
|
|
265
|
+
if secret is not None:
|
|
266
|
+
out["secret"] = secret
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
def _workflow(self, workflow_id):
|
|
270
|
+
workflows = getattr(self.runtime, "workflows", None)
|
|
271
|
+
if workflows is None:
|
|
272
|
+
raise APIError("Workflows aren't available in this Mobster.", 503, "unavailable")
|
|
273
|
+
if not isinstance(workflow_id, str) or not WORKFLOW_ID.fullmatch(workflow_id):
|
|
274
|
+
raise ValueError("workflowId is a saved workflow's id")
|
|
275
|
+
return workflows.get(workflow_id)
|
|
276
|
+
|
|
277
|
+
def list(self, workflow_id=None):
|
|
278
|
+
"""The hooks, oldest first. A hook whose workflow was removed is revoked here
|
|
279
|
+
(workflows.delete knows no hooks)."""
|
|
280
|
+
workflows = getattr(self.runtime, "workflows", None)
|
|
281
|
+
out = []
|
|
282
|
+
for row in self.store.all(workflow_id):
|
|
283
|
+
if workflows is not None:
|
|
284
|
+
try:
|
|
285
|
+
workflows.get(row["workflow_id"])
|
|
286
|
+
except APIError:
|
|
287
|
+
self.revoke(row["id"])
|
|
288
|
+
continue
|
|
289
|
+
out.append(self.public(row))
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
def create(self, workflow_id):
|
|
293
|
+
"""A new hook for the workflow: its row with ``secret``, the only time the secret is shown."""
|
|
294
|
+
self._workflow(workflow_id)
|
|
295
|
+
base = self.base_url()
|
|
296
|
+
if not base:
|
|
297
|
+
raise APIError(REMOTE_OFF, 409, "remote_off")
|
|
298
|
+
if len(self.store.all()) >= MAX_HOOKS:
|
|
299
|
+
raise APIError(f"You have {MAX_HOOKS} webhook URLs. Revoke one first.", 409, "hook_limit")
|
|
300
|
+
hook_id, secret = secrets.token_hex(16), new_secret()
|
|
301
|
+
try:
|
|
302
|
+
self.vault.put(hook_id, secret)
|
|
303
|
+
except outlet_vault.VaultError:
|
|
304
|
+
raise APIError("The Keychain didn't save the webhook's secret. Nothing was made.", 503,
|
|
305
|
+
"keychain_unavailable") from None
|
|
306
|
+
row = {"id": hook_id, "workflow_id": workflow_id, "url": f"{base}/{hook_id}", "created_at": self.clock(),
|
|
307
|
+
"uses": 0}
|
|
308
|
+
try:
|
|
309
|
+
self.store.add(row)
|
|
310
|
+
except Exception:
|
|
311
|
+
self._forget(hook_id)
|
|
312
|
+
raise
|
|
313
|
+
self._tell_remote()
|
|
314
|
+
return self.public(self.store.get(hook_id), secret)
|
|
315
|
+
|
|
316
|
+
def revoke(self, hook_id):
|
|
317
|
+
row = self.store.get(hook_id) if isinstance(hook_id, str) else None
|
|
318
|
+
if row is None:
|
|
319
|
+
raise APIError("That webhook URL was revoked.", 404, "not_found")
|
|
320
|
+
self.store.delete(hook_id)
|
|
321
|
+
self._forget(hook_id)
|
|
322
|
+
self._tell_remote()
|
|
323
|
+
return hook_id
|
|
324
|
+
|
|
325
|
+
def forget_workflow(self, workflow_id):
|
|
326
|
+
"""A removed workflow's hooks go with it."""
|
|
327
|
+
for row in self.store.all(workflow_id):
|
|
328
|
+
self.revoke(row["id"])
|
|
329
|
+
|
|
330
|
+
def _forget(self, hook_id):
|
|
331
|
+
try:
|
|
332
|
+
self.vault.delete(hook_id)
|
|
333
|
+
except outlet_vault.VaultError:
|
|
334
|
+
log.warning("hooks: the Keychain kept a revoked webhook's secret")
|
|
335
|
+
|
|
336
|
+
def _allow(self, hook_id, now):
|
|
337
|
+
with self._lock:
|
|
338
|
+
tokens, at = self._buckets.get(hook_id, (float(RATE), now))
|
|
339
|
+
tokens = min(float(RATE), tokens + (now - at) * RATE / RATE_SECONDS)
|
|
340
|
+
if tokens < 1:
|
|
341
|
+
self._buckets[hook_id] = (tokens, now)
|
|
342
|
+
return False
|
|
343
|
+
self._buckets[hook_id] = (tokens - 1, now)
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def handle(self, hook_id, signature, body):
|
|
347
|
+
"""One call: {"status": 202, "filled", "ignored"} when the workflow started,
|
|
348
|
+
else {"status", "error", "message"}."""
|
|
349
|
+
now = self.clock()
|
|
350
|
+
try:
|
|
351
|
+
answer = self._handle(hook_id, signature, body, now)
|
|
352
|
+
except Exception: # noqa: BLE001 -- an unknown dispatch outcome is never sent again with this event id
|
|
353
|
+
log.exception("hooks: a call failed")
|
|
354
|
+
answer = refuse(500, "error", "Mobster hit an error. Check task history before sending another call.")
|
|
355
|
+
if isinstance(hook_id, str) and HOOK_ID.fullmatch(hook_id) and answer["status"] != 404:
|
|
356
|
+
try:
|
|
357
|
+
self.store.used(hook_id, now, answer["status"], answer.get("message"))
|
|
358
|
+
except JournalError:
|
|
359
|
+
pass
|
|
360
|
+
return answer
|
|
361
|
+
|
|
362
|
+
def _handle(self, hook_id, signature, body, now):
|
|
363
|
+
row = self.store.get(hook_id) if isinstance(hook_id, str) and HOOK_ID.fullmatch(hook_id) else None
|
|
364
|
+
if row is None:
|
|
365
|
+
return refuse(404, "not_found", "No webhook has that address. It may have been revoked.")
|
|
366
|
+
if not isinstance(body, str) or len(body.encode()) > MAX_BODY:
|
|
367
|
+
return refuse(413, "too_large", f"The body is at most {MAX_BODY // 1024} KB.")
|
|
368
|
+
match = SIGNATURE.fullmatch(str(signature or "").strip())
|
|
369
|
+
if not match:
|
|
370
|
+
return refuse(401, "unsigned", "Sign the call: Mobster-Signature: t=<unix seconds>,v1=<HMAC-SHA256 hex>.")
|
|
371
|
+
stamp, given = int(match.group(1)), match.group(2)
|
|
372
|
+
if abs(now - stamp) > WINDOW:
|
|
373
|
+
return refuse(401, "stale", "The signature's time is more than 5 minutes from now. Sign it again.")
|
|
374
|
+
try:
|
|
375
|
+
secret = self.vault.get(hook_id)
|
|
376
|
+
except outlet_vault.VaultError:
|
|
377
|
+
return refuse(503, "keychain", "Mobster couldn't read the webhook's secret. Nothing started.")
|
|
378
|
+
if not secret:
|
|
379
|
+
return refuse(503, "keychain", "Mobster no longer has the webhook's secret. Make a new URL.")
|
|
380
|
+
expected = sign(secret, stamp, body).rsplit("v1=", 1)[1]
|
|
381
|
+
if not hmac.compare_digest(expected, given):
|
|
382
|
+
return refuse(401, "bad_signature", "The signature doesn't match. Sign <t>.<body> with the URL's secret.")
|
|
383
|
+
try:
|
|
384
|
+
data = json.loads(body) if body.strip() else {}
|
|
385
|
+
except ValueError:
|
|
386
|
+
return refuse(422, "not_json", "The body is a JSON object, such as {\"person\": \"Sam\"}.")
|
|
387
|
+
if not isinstance(data, dict):
|
|
388
|
+
return refuse(422, "not_json", "The body is a JSON object, such as {\"person\": \"Sam\"}.")
|
|
389
|
+
workflows = getattr(self.runtime, "workflows", None)
|
|
390
|
+
if workflows is None:
|
|
391
|
+
return refuse(503, "unavailable", "Workflows aren't available in this Mobster.")
|
|
392
|
+
try:
|
|
393
|
+
workflow = workflows.get(row["workflow_id"])
|
|
394
|
+
except APIError:
|
|
395
|
+
return refuse(410, "workflow_gone", "That workflow was removed. Nothing started.")
|
|
396
|
+
try:
|
|
397
|
+
event = event_envelope(data, workflow, workflows)
|
|
398
|
+
except ValueError as error:
|
|
399
|
+
return refuse(422, "invalid_event", str(error))
|
|
400
|
+
if self.store.seen(hook_id, given, now):
|
|
401
|
+
return refuse(409, "replayed", "This exact call came before, so nothing started again.")
|
|
402
|
+
if not self._allow(hook_id, now):
|
|
403
|
+
return refuse(429, "rate_limited", f"At most {RATE} calls a minute to one webhook URL. Try again soon.")
|
|
404
|
+
if event is None:
|
|
405
|
+
values, ignored = fill(workflow, data)
|
|
406
|
+
request_key = f"hook-{hook_id[:8]}-{given[:40]}"
|
|
407
|
+
else:
|
|
408
|
+
event_id, values, digest = event
|
|
409
|
+
result = self.store.claim_event(hook_id, event_id, digest, now)
|
|
410
|
+
if result == "gone":
|
|
411
|
+
return refuse(404, "not_found", "No webhook has that address. It may have been revoked.")
|
|
412
|
+
if result == "full":
|
|
413
|
+
return refuse(503, "event_limit", "This Mac has reached its saved webhook event limit. Revoke an "
|
|
414
|
+
"unused webhook URL before sending more events.")
|
|
415
|
+
if result == "changed":
|
|
416
|
+
return refuse(409, "event_changed", "This event id was already used with different content.")
|
|
417
|
+
if result == "replayed":
|
|
418
|
+
return refuse(409, "replayed", "This event came before, so nothing started again.")
|
|
419
|
+
ignored = []
|
|
420
|
+
request_key = f"hook-{hook_id}-event-{hashlib.sha256(event_id.encode()).hexdigest()}"
|
|
421
|
+
# origin "hook" (harness_api.FORCED_ASK_ORIGINS) asks before it sends, buys, posts or deletes; nothing in a
|
|
422
|
+
# body can change the steps, the app, the phone or where the result goes.
|
|
423
|
+
try:
|
|
424
|
+
if event is None:
|
|
425
|
+
workflows.run(workflow["id"], request_key, values or None, origin="hook", body=data)
|
|
426
|
+
else:
|
|
427
|
+
workflows.run_hook_event(workflow["id"], request_key, values or None, data,
|
|
428
|
+
data["_mobsterEvent"]["occurredAt"])
|
|
429
|
+
except APIError as error:
|
|
430
|
+
return refuse(error.status if 400 <= error.status < 600 else 409, error.code, str(error))
|
|
431
|
+
except ValueError as error:
|
|
432
|
+
return refuse(422, "blanks", str(error))
|
|
433
|
+
return {"status": 202, "filled": sorted(values), "ignored": ignored[:20],
|
|
434
|
+
"message": f"Started {workflow.get('name') or 'the workflow'}."}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def refuse(status, error, message):
|
|
438
|
+
return {"status": status, "error": error, "message": message}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
_services = ServiceRegistry()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def service_for(runtime):
|
|
445
|
+
return _services.get(runtime)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def start(runtime):
|
|
449
|
+
_services.add(runtime, HookService(runtime))
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def close(runtime):
|
|
453
|
+
_services.pop(runtime)
|