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,316 @@
|
|
|
1
|
+
"""A webhook destination's address, the JSON a run sends it, the signature, and one POST."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import datetime
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import io
|
|
8
|
+
import ipaddress
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
|
|
13
|
+
from .. import secret_filter
|
|
14
|
+
from . import net
|
|
15
|
+
|
|
16
|
+
TIMEOUT = 10.0
|
|
17
|
+
URL_MAX = 2000
|
|
18
|
+
SECRET_MAX = 200
|
|
19
|
+
NAME_MAX = 60
|
|
20
|
+
ANSWER_MAX = 20_000
|
|
21
|
+
TEXT_MAX = 3000
|
|
22
|
+
SCREENSHOT_SIDE = 1280
|
|
23
|
+
USER_AGENT = "Mobster-Webhooks/1"
|
|
24
|
+
# The run statuses that finished the task: no reason goes with them.
|
|
25
|
+
DONE = frozenset({"completed", "completed_unverified", "user_condition_met"})
|
|
26
|
+
LOCAL_SUFFIXES = (".local", ".localhost", ".lan", ".home.arpa", ".internal")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AddressError(ValueError):
|
|
30
|
+
"""Why an address can't take results, in one sentence that never repeats the address."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _literal(host):
|
|
34
|
+
try:
|
|
35
|
+
return ipaddress.ip_address(host.strip("[]"))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _private(host):
|
|
41
|
+
"""This Mac or a private network: localhost, a name with no dot or a .local one, or a loopback, private or
|
|
42
|
+
link-local address."""
|
|
43
|
+
if host == "localhost" or host.endswith(LOCAL_SUFFIXES) or ("." not in host and ":" not in host):
|
|
44
|
+
return True
|
|
45
|
+
address = _literal(host)
|
|
46
|
+
if address is None:
|
|
47
|
+
return False
|
|
48
|
+
if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
|
|
49
|
+
address = address.ipv4_mapped
|
|
50
|
+
return address.is_loopback or address.is_private or address.is_link_local
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_url(value):
|
|
54
|
+
"""``value`` trimmed, when results may go there; AddressError otherwise."""
|
|
55
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
56
|
+
if not text:
|
|
57
|
+
raise AddressError("Paste the webhook's address.")
|
|
58
|
+
if len(text) > URL_MAX or any(ord(c) < 33 or ord(c) == 127 for c in text):
|
|
59
|
+
raise AddressError("That address isn't a web address.")
|
|
60
|
+
parts = urlsplit(text)
|
|
61
|
+
scheme = parts.scheme.lower()
|
|
62
|
+
if scheme not in ("https", "http"):
|
|
63
|
+
raise AddressError("Use an address that starts with https://.")
|
|
64
|
+
if parts.username or parts.password:
|
|
65
|
+
raise AddressError("Put the secret in Secret, not in the address.")
|
|
66
|
+
host = (parts.hostname or "").rstrip(".").lower()
|
|
67
|
+
if not host:
|
|
68
|
+
raise AddressError("That address has no host.")
|
|
69
|
+
try:
|
|
70
|
+
parts.port
|
|
71
|
+
except ValueError:
|
|
72
|
+
raise AddressError("That address has a port Mobster can't use.") from None
|
|
73
|
+
if scheme == "http" and not _private(host):
|
|
74
|
+
raise AddressError("Use https:// for an address on the internet. http:// works for this Mac and your own "
|
|
75
|
+
"network.")
|
|
76
|
+
return text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def hint(url):
|
|
80
|
+
"""What the app shows of an address: its scheme, host and first path segment ("https://hooks.zapier.com/hooks/…").
|
|
81
|
+
The rest often carries the address's own key."""
|
|
82
|
+
parts = urlsplit(url)
|
|
83
|
+
host = parts.hostname or ""
|
|
84
|
+
if ":" in host:
|
|
85
|
+
host = f"[{host}]"
|
|
86
|
+
if parts.port:
|
|
87
|
+
host += f":{parts.port}"
|
|
88
|
+
segments = [s for s in parts.path.split("/") if s]
|
|
89
|
+
shown = f"{parts.scheme.lower()}://{host}"
|
|
90
|
+
if segments:
|
|
91
|
+
first = segments[0] if len(segments[0]) <= 24 else segments[0][:24] + "…"
|
|
92
|
+
shown += "/" + first + ("/…" if len(segments) > 1 or parts.query else "")
|
|
93
|
+
elif parts.query:
|
|
94
|
+
shown += "/…"
|
|
95
|
+
return shown
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
host_of = net.host_of
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_secret(value):
|
|
102
|
+
if value is None or value == "":
|
|
103
|
+
return None
|
|
104
|
+
if (not isinstance(value, str) or not 8 <= len(value) <= SECRET_MAX
|
|
105
|
+
or any(ord(c) < 33 or ord(c) == 127 for c in value)):
|
|
106
|
+
raise ValueError("A secret is 8 to 200 characters with no spaces.")
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_name(value, example="My server"):
|
|
111
|
+
"""The destination's name, spaces folded. ``example``: a name for its kind, for the error (kinds.Kind.example)."""
|
|
112
|
+
text = " ".join(value.split()) if isinstance(value, str) else ""
|
|
113
|
+
if not 1 <= len(text) <= NAME_MAX:
|
|
114
|
+
raise ValueError(f"Name the destination in 1 to {NAME_MAX} characters, like {example}.")
|
|
115
|
+
return text
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def iso(seconds):
|
|
119
|
+
if not isinstance(seconds, (int, float)):
|
|
120
|
+
return None
|
|
121
|
+
return datetime.datetime.fromtimestamp(seconds, datetime.timezone.utc).isoformat(timespec="seconds").replace(
|
|
122
|
+
"+00:00", "Z")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def masked(value, depth=0):
|
|
126
|
+
"""``value`` with secret_filter.redact over every string inside it (keys too). Depth past 20 becomes None."""
|
|
127
|
+
if depth > 20:
|
|
128
|
+
return None
|
|
129
|
+
if isinstance(value, str):
|
|
130
|
+
return secret_filter.redact(value)
|
|
131
|
+
if isinstance(value, dict):
|
|
132
|
+
return {secret_filter.redact(str(k)): masked(v, depth + 1) for k, v in value.items()}
|
|
133
|
+
if isinstance(value, (list, tuple)):
|
|
134
|
+
return [masked(v, depth + 1) for v in value]
|
|
135
|
+
if isinstance(value, bool) or value is None or isinstance(value, (int, float)):
|
|
136
|
+
return value
|
|
137
|
+
return secret_filter.redact(str(value))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def answer_of(summary):
|
|
141
|
+
"""The run's whole answer as text: the answer it wrote in full, else its text data, else its headline."""
|
|
142
|
+
for key in ("full_answer", "data", "answer"):
|
|
143
|
+
value = summary.get(key)
|
|
144
|
+
if isinstance(value, str) and value.strip():
|
|
145
|
+
return value.strip()[:ANSWER_MAX]
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def data_of(summary):
|
|
150
|
+
"""The structured data: an output schema's (or a format's) data once it was validated; None for a text answer."""
|
|
151
|
+
data = summary.get("data")
|
|
152
|
+
if summary.get("data_status") == "validated" and data is not None and not isinstance(data, str):
|
|
153
|
+
return data
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def line_of(title, status, answer, reason):
|
|
158
|
+
"""``text``: the answer; without one, Done or why it couldn't finish."""
|
|
159
|
+
if answer:
|
|
160
|
+
said = answer
|
|
161
|
+
elif status in DONE:
|
|
162
|
+
said = "Done."
|
|
163
|
+
else:
|
|
164
|
+
said = f"Couldn't finish. {reason}" if reason else "Couldn't finish."
|
|
165
|
+
text = f"{title}: {' '.join(said.split())}"
|
|
166
|
+
return text if len(text) <= TEXT_MAX else text[:TEXT_MAX - 1] + "…"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def payload(run, workflow=None, screenshot=None):
|
|
170
|
+
"""The body for ``run``: an object as the module docstring shows."""
|
|
171
|
+
summary = run.summary if isinstance(getattr(run, "summary", None), dict) else {}
|
|
172
|
+
status = getattr(run, "status", None) or summary.get("status") or "error"
|
|
173
|
+
answer = masked(answer_of(summary))
|
|
174
|
+
reason = summary.get("reason") if status not in DONE else None
|
|
175
|
+
reason = masked(" ".join(reason.split())) if isinstance(reason, str) and reason.strip() else None
|
|
176
|
+
flow = None
|
|
177
|
+
if isinstance(workflow, dict) and workflow.get("name"):
|
|
178
|
+
blanks = workflow.get("blanks") if isinstance(workflow.get("blanks"), dict) else {}
|
|
179
|
+
flow = {"id": workflow.get("id"), "name": masked(workflow["name"]), "blanks": masked(blanks)}
|
|
180
|
+
body = {"type": "run.finished", "runId": run.id, "status": status, "answer": answer,
|
|
181
|
+
"data": masked(data_of(summary)), "reason": reason, "startedAt": iso(getattr(run, "created_at", None)),
|
|
182
|
+
"finishedAt": iso(getattr(run, "finished_at", None)), "workflow": flow,
|
|
183
|
+
"text": line_of(f"Mobster › {flow['name']}" if flow else "Mobster", status, answer, reason)}
|
|
184
|
+
if screenshot:
|
|
185
|
+
body["screenshot"] = {"contentType": "image/jpeg", "base64": base64.b64encode(screenshot).decode()}
|
|
186
|
+
return body
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def started_payload(run, workflow=None):
|
|
190
|
+
"""run.started: which run, when, and its workflow when it came from one. No task words."""
|
|
191
|
+
flow = _flow(workflow)
|
|
192
|
+
title = f"Mobster › {flow['name']}" if flow else "Mobster"
|
|
193
|
+
return {"type": "run.started", "runId": run.id, "startedAt": iso(getattr(run, "created_at", None)),
|
|
194
|
+
"workflow": flow, "text": f"{title}: a task started."}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# approval_requested kinds (server.request_approval) and what a needs_you event calls them.
|
|
198
|
+
NEED_KINDS = {"action": "approval", "commit": "approval", "proposals": "approval", "clarify": "question",
|
|
199
|
+
"loop": "question", "question": "question", "plan": "plan"}
|
|
200
|
+
QUESTION_MAX = 300
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def needs_payload(run, event, workflow=None, now=None):
|
|
204
|
+
"""needs_you: the run is waiting for an answer in the app. ``event`` is its approval_requested event."""
|
|
205
|
+
now = time.time() if now is None else now
|
|
206
|
+
flow = _flow(workflow)
|
|
207
|
+
# What the card asks, masked: never the message, a typed answer or anything the card doesn't say.
|
|
208
|
+
asked = " ".join(str(event.get("title") or event.get("label") or "").split())[:QUESTION_MAX]
|
|
209
|
+
question = secret_filter.redact(asked) or "Mobster needs your answer in the app."
|
|
210
|
+
title = f"Mobster › {flow['name']}" if flow else "Mobster"
|
|
211
|
+
return {"type": "needs_you", "runId": run.id, "kind": NEED_KINDS.get(event.get("kind") or "action", "approval"),
|
|
212
|
+
"question": question, "askedAt": iso(now), "workflow": flow,
|
|
213
|
+
"text": f"{title} needs you: {question}"[:TEXT_MAX]}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _flow(workflow):
|
|
217
|
+
if isinstance(workflow, dict) and workflow.get("name"):
|
|
218
|
+
blanks = workflow.get("blanks") if isinstance(workflow.get("blanks"), dict) else {}
|
|
219
|
+
return {"id": workflow.get("id"), "name": masked(workflow["name"]), "blanks": masked(blanks)}
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_payload(now=None):
|
|
224
|
+
now = time.time() if now is None else now
|
|
225
|
+
return {"type": "test", "runId": None, "status": "completed",
|
|
226
|
+
"answer": "This is a test from Mobster. A finished task's answer goes here.", "data": None, "reason": None,
|
|
227
|
+
"startedAt": iso(now), "finishedAt": iso(now), "workflow": None,
|
|
228
|
+
"text": "Mobster: this is a test. Results from your tasks will arrive here."}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def slack_text(text):
|
|
232
|
+
"""Slack's escaping for its text field: &, < and > only."""
|
|
233
|
+
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def encode_body(url, body):
|
|
237
|
+
"""The bytes to send. Slack's incoming webhooks read ``text`` as their own markup, so it's escaped for them."""
|
|
238
|
+
host = host_of(url)
|
|
239
|
+
if host == "hooks.slack.com" or host.endswith(".slack.com"):
|
|
240
|
+
body = {**body, "text": slack_text(body.get("text") or "")}
|
|
241
|
+
return json.dumps(body, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode("utf-8")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def signature(secret, timestamp, data):
|
|
245
|
+
"""v1: HMAC-SHA256 with ``secret`` over ``<timestamp>.<body>``, in hex."""
|
|
246
|
+
message = str(int(timestamp)).encode() + b"." + data
|
|
247
|
+
return hmac.new(secret.encode("utf-8"), message, hashlib.sha256).hexdigest()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def headers(event, delivery_id, data, secret=None, timestamp=None):
|
|
251
|
+
timestamp = int(time.time() if timestamp is None else timestamp)
|
|
252
|
+
out = {"Content-Type": "application/json", "User-Agent": USER_AGENT, "Mobster-Event": event,
|
|
253
|
+
"Mobster-Delivery": delivery_id, "Mobster-Timestamp": str(timestamp)}
|
|
254
|
+
if secret:
|
|
255
|
+
out["Mobster-Signature"] = f"t={timestamp},v1={signature(secret, timestamp, data)}"
|
|
256
|
+
return out
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def screenshot_of(run, frames=None):
|
|
260
|
+
"""The run's last screen as JPEG bytes (at most SCREENSHOT_SIDE px), or None: the final full screenshot while the
|
|
261
|
+
app still has it, else the last step's saved frame."""
|
|
262
|
+
image = getattr(run, "image", None)
|
|
263
|
+
if isinstance(image, str) and image.startswith("data:image/") and "," in image:
|
|
264
|
+
try:
|
|
265
|
+
raw = base64.b64decode(image.split(",", 1)[1])
|
|
266
|
+
from PIL import Image
|
|
267
|
+
picture = Image.open(io.BytesIO(raw)).convert("RGB")
|
|
268
|
+
picture.thumbnail((SCREENSHOT_SIDE, SCREENSHOT_SIDE))
|
|
269
|
+
out = io.BytesIO()
|
|
270
|
+
picture.save(out, "JPEG", quality=80)
|
|
271
|
+
return out.getvalue()
|
|
272
|
+
except Exception: # noqa: BLE001 -- the step frame below, or none
|
|
273
|
+
pass
|
|
274
|
+
if frames is None:
|
|
275
|
+
return None
|
|
276
|
+
for event in reversed(list(getattr(run, "events", None) or ())):
|
|
277
|
+
frame = event.get("frameId") if isinstance(event, dict) else None
|
|
278
|
+
if isinstance(frame, str):
|
|
279
|
+
try:
|
|
280
|
+
data = frames.get(run.id, frame)
|
|
281
|
+
except Exception: # noqa: BLE001
|
|
282
|
+
data = None
|
|
283
|
+
if data:
|
|
284
|
+
return data
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class Outcome:
|
|
289
|
+
"""What one try came to: ``ok``, ``retry`` (worth another try), the HTTP ``status`` and a ``message``. ``unsent``:
|
|
290
|
+
the try surely never reached the destination, so another can't make a duplicate."""
|
|
291
|
+
|
|
292
|
+
def __init__(self, ok, retry, status=None, message=None, unsent=False):
|
|
293
|
+
self.ok, self.retry, self.status, self.message, self.unsent = ok, retry, status, message, unsent
|
|
294
|
+
|
|
295
|
+
def __repr__(self):
|
|
296
|
+
return f"Outcome(ok={self.ok}, retry={self.retry}, status={self.status}, message={self.message!r})"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def post(url, data, request_headers, timeout=TIMEOUT, connect=net.connection):
|
|
300
|
+
"""POST ``data`` to ``url`` once. Never raises; never names the address beyond its host."""
|
|
301
|
+
try:
|
|
302
|
+
reply = net.call("POST", url, data, request_headers, timeout, connect, read_limit=4096)
|
|
303
|
+
except net.NetError as error:
|
|
304
|
+
return Outcome(False, error.retry, None, error.message, unsent=error.unsent)
|
|
305
|
+
return judge(reply.status)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def judge(status):
|
|
309
|
+
if 200 <= status < 300:
|
|
310
|
+
return Outcome(True, False, status, None)
|
|
311
|
+
if 300 <= status < 400:
|
|
312
|
+
return Outcome(False, False, status, f"The address answered {status}, a redirect. Use the address it points "
|
|
313
|
+
"to.")
|
|
314
|
+
if status in (408, 425, 429) or status >= 500:
|
|
315
|
+
return Outcome(False, True, status, f"The address answered {status}.")
|
|
316
|
+
return Outcome(False, False, status, f"The address answered {status}. Check it in Settings › Connectors.")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Result presentation contracts, and the bounded JSON Schema subset a task's output may declare.
|
|
2
|
+
|
|
3
|
+
The schema subset is safe on purpose: it never fetches references, executes regular expressions or infers missing
|
|
4
|
+
values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from .transport import decode_json
|
|
10
|
+
|
|
11
|
+
KEYWORDS = frozenset({"$schema", "type", "properties", "required", "additionalProperties",
|
|
12
|
+
"items", "minItems", "maxItems", "uniqueItems", "minLength", "maxLength", "minimum",
|
|
13
|
+
"maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "enum", "const",
|
|
14
|
+
"title", "description"})
|
|
15
|
+
TYPES = frozenset({"object", "array", "string", "number", "integer", "boolean", "null"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bounded_json(value, *, max_bytes=32000, max_nodes=2000, max_depth=12):
|
|
19
|
+
pending, count = [(value, 0)], 0
|
|
20
|
+
while pending:
|
|
21
|
+
node, depth = pending.pop()
|
|
22
|
+
count += 1
|
|
23
|
+
if count > max_nodes or depth > max_depth:
|
|
24
|
+
raise ValueError("JSON exceeds structural limits")
|
|
25
|
+
if type(node) is dict:
|
|
26
|
+
if any(not isinstance(key, str) for key in node):
|
|
27
|
+
raise ValueError("JSON object keys must be strings")
|
|
28
|
+
pending.extend((child, depth + 1) for child in node.values())
|
|
29
|
+
elif type(node) is list:
|
|
30
|
+
pending.extend((child, depth + 1) for child in node)
|
|
31
|
+
elif node is not None and type(node) not in (str, int, float, bool):
|
|
32
|
+
raise ValueError("Result contains a non-JSON value")
|
|
33
|
+
try:
|
|
34
|
+
wire = json.dumps(value, allow_nan=False, sort_keys=True, separators=(",", ":"))
|
|
35
|
+
if len(wire.encode()) > max_bytes:
|
|
36
|
+
raise ValueError("JSON exceeds byte limit")
|
|
37
|
+
return decode_json(wire)
|
|
38
|
+
except (TypeError, OverflowError, UnicodeError):
|
|
39
|
+
raise ValueError("Invalid JSON value") from None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_schema(schema):
|
|
43
|
+
schema = bounded_json(schema, max_bytes=16000, max_nodes=500, max_depth=12)
|
|
44
|
+
if not isinstance(schema, dict):
|
|
45
|
+
raise ValueError("Output schema must be a JSON Schema object")
|
|
46
|
+
try:
|
|
47
|
+
from jsonschema import Draft202012Validator
|
|
48
|
+
except ImportError:
|
|
49
|
+
raise ValueError("Structured output requires mobile_agent/requirements.txt") from None
|
|
50
|
+
|
|
51
|
+
def inspect(node, depth=0):
|
|
52
|
+
if not isinstance(node, dict) or depth > 6 or set(node) - KEYWORDS:
|
|
53
|
+
raise ValueError("Unsupported output schema keyword or nesting depth")
|
|
54
|
+
declared = node.get("type")
|
|
55
|
+
kinds = declared if isinstance(declared, list) else [declared]
|
|
56
|
+
if not kinds or any(not isinstance(kind, str) or kind not in TYPES for kind in kinds):
|
|
57
|
+
raise ValueError("Every output schema node requires an explicit supported type")
|
|
58
|
+
if "$schema" in node and node["$schema"] != "https://json-schema.org/draft/2020-12/schema":
|
|
59
|
+
raise ValueError("Only JSON Schema Draft 2020-12 is supported")
|
|
60
|
+
if "object" in kinds:
|
|
61
|
+
properties = node.get("properties", {})
|
|
62
|
+
if (not isinstance(properties, dict) or len(properties) > 40
|
|
63
|
+
or node.get("additionalProperties") is not False):
|
|
64
|
+
raise ValueError("Objects require bounded properties and additionalProperties:false")
|
|
65
|
+
required = node.get("required", [])
|
|
66
|
+
if (not isinstance(required, list)
|
|
67
|
+
or any(not isinstance(key, str) or key not in properties for key in required)):
|
|
68
|
+
raise ValueError("Required properties must have a defined schema")
|
|
69
|
+
for child in properties.values():
|
|
70
|
+
inspect(child, depth + 1)
|
|
71
|
+
elif "properties" in node or "required" in node or "additionalProperties" in node:
|
|
72
|
+
raise ValueError("Object keywords require object type")
|
|
73
|
+
if "array" in kinds:
|
|
74
|
+
if "items" not in node:
|
|
75
|
+
raise ValueError("Arrays require a bounded item schema")
|
|
76
|
+
inspect(node["items"], depth + 1)
|
|
77
|
+
if type(node.get("maxItems")) is not int or not 0 <= node["maxItems"] <= 100:
|
|
78
|
+
raise ValueError("Arrays require maxItems between 0 and 100")
|
|
79
|
+
elif "items" in node:
|
|
80
|
+
raise ValueError("Array items require array type")
|
|
81
|
+
if "enum" in node and (not isinstance(node["enum"], list) or not 1 <= len(node["enum"]) <= 100):
|
|
82
|
+
raise ValueError("Enum exceeds supported bounds")
|
|
83
|
+
|
|
84
|
+
inspect(schema)
|
|
85
|
+
try:
|
|
86
|
+
Draft202012Validator.check_schema(schema)
|
|
87
|
+
except Exception:
|
|
88
|
+
raise ValueError("Invalid JSON Schema") from None
|
|
89
|
+
return schema
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
FORMATS = frozenset({"text", "json", "yaml", "csv", "markdown"})
|
|
94
|
+
SCALAR_TYPES = frozenset({"string", "number", "integer", "boolean", "null"})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_format(output_format):
|
|
98
|
+
"""An actual result format; Auto is a request policy, never a serializer."""
|
|
99
|
+
if not isinstance(output_format, str) or output_format not in FORMATS:
|
|
100
|
+
raise ValueError("Choose Text, JSON, YAML, CSV, or Markdown output")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def validate_output(output_format, schema):
|
|
104
|
+
if output_format == "auto":
|
|
105
|
+
if schema is not None:
|
|
106
|
+
raise ValueError("Choose a structured format to supply a custom schema")
|
|
107
|
+
return None
|
|
108
|
+
validate_format(output_format)
|
|
109
|
+
if output_format == "text" and schema is not None:
|
|
110
|
+
raise ValueError("Choose a structured format to supply a custom schema")
|
|
111
|
+
schema = validate_schema(schema) if schema is not None else None
|
|
112
|
+
if output_format != "csv" or schema is None:
|
|
113
|
+
return schema
|
|
114
|
+
return _validate_csv_shape(schema)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_automatic_schema(output_format, schema):
|
|
118
|
+
"""A model-selected schema is mandatory and bounded, including plain-text answers."""
|
|
119
|
+
validate_format(output_format)
|
|
120
|
+
schema = validate_schema(schema)
|
|
121
|
+
if output_format == "csv":
|
|
122
|
+
return _validate_csv_shape(schema)
|
|
123
|
+
if output_format == "text" and schema["type"] != "string":
|
|
124
|
+
# Automatic text is one string or one flat object; a table is CSV-only.
|
|
125
|
+
if schema["type"] == "array":
|
|
126
|
+
raise ValueError("Text requires a string or a flat object with named columns")
|
|
127
|
+
return _validate_csv_shape(schema)
|
|
128
|
+
return schema
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _validate_csv_shape(schema):
|
|
132
|
+
row = schema.get("items") if schema.get("type") == "array" else schema
|
|
133
|
+
if (not isinstance(row, dict) or row.get("type") != "object"
|
|
134
|
+
or row.get("additionalProperties") is not False or not row.get("properties")):
|
|
135
|
+
raise ValueError("CSV requires a flat object or an array of flat objects with named columns")
|
|
136
|
+
for column in row["properties"].values():
|
|
137
|
+
declared = column["type"]
|
|
138
|
+
kinds = declared if isinstance(declared, list) else [declared]
|
|
139
|
+
if any(kind not in SCALAR_TYPES for kind in kinds):
|
|
140
|
+
raise ValueError("CSV columns must be scalar values, not nested objects or arrays")
|
|
141
|
+
return schema
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Whether the person who owns an iPhone is using it, so Mobster never grabs it from them."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
POLL_INTERVAL_S = 15.0 # between read-only polls of one phone
|
|
7
|
+
READ_TIMEOUT_S = 3.0
|
|
8
|
+
ACTION_QUIET_S = 30.0 # a change this soon after a Mobster action is Mobster's own
|
|
9
|
+
OWNER_QUIET_S = 120.0 # owner_active ends after this long with no change
|
|
10
|
+
IDLE_AFTER_S = 300.0 # a phone first seen, or unchanged, is idle after this long
|
|
11
|
+
IDLE_RELEASE_S = 120.0 # idle for this long: stop live view, prewarm and the awake hold
|
|
12
|
+
|
|
13
|
+
OWNER_ACTIVE_REFUSAL = ("You're using your iPhone right now, so Mobster didn't start this task. "
|
|
14
|
+
"No action was taken. It can start once you put the phone down.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OwnerPresence:
|
|
18
|
+
"""One phone's presence. ``observe`` is the only input besides ``note_mobster_action``/``run_started``."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, clock=time.time):
|
|
21
|
+
self.clock = clock
|
|
22
|
+
self.lock = threading.Lock()
|
|
23
|
+
self.state, self.since = "unknown", self._ms(clock())
|
|
24
|
+
self.baseline = None # (locked, front) at the last read
|
|
25
|
+
self.first_seen = None
|
|
26
|
+
self.last_change = None
|
|
27
|
+
self.last_action = None
|
|
28
|
+
self.holding = False
|
|
29
|
+
self.idle_since = None
|
|
30
|
+
self.released = False # idle release already done for this idle stretch
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _ms(seconds):
|
|
34
|
+
return int(seconds * 1000)
|
|
35
|
+
|
|
36
|
+
def _set(self, state, now):
|
|
37
|
+
if state != self.state:
|
|
38
|
+
self.state, self.since = state, self._ms(now)
|
|
39
|
+
self.released = False
|
|
40
|
+
self.idle_since = now if state == "idle" else None
|
|
41
|
+
|
|
42
|
+
# -- inputs -----------------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def note_mobster_action(self, now=None):
|
|
45
|
+
"""Mobster acted on the phone (a run's step, an MCP tap, manual control): a change soon after is its own."""
|
|
46
|
+
with self.lock:
|
|
47
|
+
self.last_action = self.clock() if now is None else now
|
|
48
|
+
|
|
49
|
+
def run_started(self, now=None):
|
|
50
|
+
with self.lock:
|
|
51
|
+
now = self.clock() if now is None else now
|
|
52
|
+
self.holding, self.last_action = True, now
|
|
53
|
+
self._set("mobster", now)
|
|
54
|
+
|
|
55
|
+
def run_finished(self, locked=None, front=None, now=None):
|
|
56
|
+
"""The run ended. What the phone shows now is Mobster's doing, not the owner's: it becomes the baseline."""
|
|
57
|
+
with self.lock:
|
|
58
|
+
now = self.clock() if now is None else now
|
|
59
|
+
self.holding, self.last_action = False, now
|
|
60
|
+
if locked is not None or front is not None:
|
|
61
|
+
self.baseline = (locked, front)
|
|
62
|
+
self.last_change = now
|
|
63
|
+
self._set("idle", now)
|
|
64
|
+
|
|
65
|
+
def observe(self, *, held=False, ok=True, locked=None, front=None, now=None):
|
|
66
|
+
with self.lock:
|
|
67
|
+
now = self.clock() if now is None else now
|
|
68
|
+
if held:
|
|
69
|
+
self.holding, self.last_action = True, now
|
|
70
|
+
self._set("mobster", now)
|
|
71
|
+
return self.state
|
|
72
|
+
if self.holding:
|
|
73
|
+
# A hold just ended without run_finished (another process's lease): treat it as a finished run.
|
|
74
|
+
self.holding, self.last_action = False, now
|
|
75
|
+
if ok:
|
|
76
|
+
self.baseline, self.last_change = (locked, front), now
|
|
77
|
+
self._set("idle", now)
|
|
78
|
+
return self.state
|
|
79
|
+
if not ok or not isinstance(locked, bool):
|
|
80
|
+
self._set("unknown", now)
|
|
81
|
+
return self.state
|
|
82
|
+
reading = (locked, front)
|
|
83
|
+
if self.first_seen is None:
|
|
84
|
+
self.first_seen = now
|
|
85
|
+
if self.baseline is None:
|
|
86
|
+
self.baseline, self.last_change = reading, now
|
|
87
|
+
elif reading != self.baseline:
|
|
88
|
+
self.baseline = reading
|
|
89
|
+
# Only a person unlocks a phone or switches its app. The phone locking itself (auto-lock) is not use.
|
|
90
|
+
if locked is False:
|
|
91
|
+
self.last_change = now
|
|
92
|
+
own = self.last_action is not None and now - self.last_action <= ACTION_QUIET_S
|
|
93
|
+
if not own:
|
|
94
|
+
self._set("owner_active", now)
|
|
95
|
+
return self.state
|
|
96
|
+
quiet = now - (self.last_change if self.last_change is not None else self.first_seen)
|
|
97
|
+
if self.state == "owner_active":
|
|
98
|
+
if quiet >= OWNER_QUIET_S:
|
|
99
|
+
self._set("idle", now)
|
|
100
|
+
elif quiet >= IDLE_AFTER_S or self.state == "idle":
|
|
101
|
+
self._set("idle", now)
|
|
102
|
+
else:
|
|
103
|
+
self._set("unknown", now)
|
|
104
|
+
return self.state
|
|
105
|
+
|
|
106
|
+
# -- outputs ----------------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def is_owner_active(self):
|
|
109
|
+
with self.lock:
|
|
110
|
+
return self.state == "owner_active"
|
|
111
|
+
|
|
112
|
+
def should_release(self, now=None):
|
|
113
|
+
with self.lock:
|
|
114
|
+
now = self.clock() if now is None else now
|
|
115
|
+
if self.state != "idle" or self.released or self.idle_since is None:
|
|
116
|
+
return False
|
|
117
|
+
if now - self.idle_since < IDLE_RELEASE_S:
|
|
118
|
+
return False
|
|
119
|
+
self.released = True
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def snapshot(self):
|
|
123
|
+
with self.lock:
|
|
124
|
+
return {"state": self.state, "since": self.since}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# -- the reads ---------------------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def read_phone(wda_url, timeout=READ_TIMEOUT_S):
|
|
131
|
+
"""(ok, locked, front): the two read-only sessionless reads. Never raises."""
|
|
132
|
+
from .transport import read_kept # the helper's kept connection: a connection a poll ran WDA out of descriptors
|
|
133
|
+
try:
|
|
134
|
+
locked = read_kept(wda_url, "/wda/locked", timeout=timeout).get("value")
|
|
135
|
+
front = None
|
|
136
|
+
if locked is False:
|
|
137
|
+
value = read_kept(wda_url, "/wda/activeAppInfo", timeout=timeout).get("value")
|
|
138
|
+
bundle = value.get("bundleId") if isinstance(value, dict) else None
|
|
139
|
+
front = bundle if isinstance(bundle, str) and bundle else None
|
|
140
|
+
return isinstance(locked, bool), locked, front
|
|
141
|
+
except Exception: # noqa: BLE001 -- an unanswered phone is "unknown", never an error
|
|
142
|
+
return False, None, None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def lease_held(wda_url):
|
|
146
|
+
"""Whether a Mobster process holds this phone's lease now (taken and let go at once). Fails to True."""
|
|
147
|
+
from .journal import Lease
|
|
148
|
+
try:
|
|
149
|
+
lease = Lease.device(wda_url.rstrip("/"), wait=0)
|
|
150
|
+
except Exception: # noqa: BLE001 -- can't tell: treat it as held, never as free
|
|
151
|
+
return True
|
|
152
|
+
lease.close()
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Poller:
|
|
157
|
+
"""Polls one phone's presence lazily: at most every POLL_INTERVAL_S, and never reads while a run holds it."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, presence, wda_url, busy=lambda: False, reader=read_phone, lease=lease_held,
|
|
160
|
+
monotonic=time.monotonic):
|
|
161
|
+
self.presence, self.wda_url, self.busy = presence, wda_url, busy
|
|
162
|
+
self.reader, self.lease, self.monotonic = reader, lease, monotonic
|
|
163
|
+
self.polled_at = None
|
|
164
|
+
|
|
165
|
+
def poll(self, force=False):
|
|
166
|
+
now = self.monotonic()
|
|
167
|
+
if not force and self.polled_at is not None and now - self.polled_at < POLL_INTERVAL_S:
|
|
168
|
+
return self.presence.snapshot()
|
|
169
|
+
self.polled_at = now
|
|
170
|
+
try:
|
|
171
|
+
held = bool(self.busy()) or self.lease(self.wda_url)
|
|
172
|
+
except Exception: # noqa: BLE001
|
|
173
|
+
held = True
|
|
174
|
+
from .wda.watchdog import tracker
|
|
175
|
+
# an MCP tap or a manual-control gesture is Mobster's, not the owner's
|
|
176
|
+
mine = tracker.last_action_at(self.wda_url)
|
|
177
|
+
if mine is not None:
|
|
178
|
+
self.presence.note_mobster_action(mine)
|
|
179
|
+
if held:
|
|
180
|
+
self.presence.observe(held=True)
|
|
181
|
+
else:
|
|
182
|
+
ok, locked, front = self.reader(self.wda_url)
|
|
183
|
+
self.presence.observe(ok=ok, locked=locked, front=front)
|
|
184
|
+
return self.presence.snapshot()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# -- a registry, so a guard or an MCP tool finds a phone's presence from its WDA address --------------
|
|
188
|
+
|
|
189
|
+
_registry = {}
|
|
190
|
+
_registry_lock = threading.Lock()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def register(wda_url, presence):
|
|
194
|
+
with _registry_lock:
|
|
195
|
+
_registry[(wda_url or "").rstrip("/")] = presence
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def presence_for(wda_url):
|
|
199
|
+
with _registry_lock:
|
|
200
|
+
return _registry.get((wda_url or "").rstrip("/"))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def is_owner_active(wda_url):
|
|
204
|
+
"""Whether the owner is using the phone at ``wda_url``. False when nothing tracks it."""
|
|
205
|
+
presence = presence_for(wda_url)
|
|
206
|
+
return bool(presence is not None and presence.is_owner_active())
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def forget(wda_url):
|
|
210
|
+
with _registry_lock:
|
|
211
|
+
_registry.pop((wda_url or "").rstrip("/"), None)
|