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
mobile_agent/fanout.py
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"""One task on several phones at once: one run per phone through Runtime.create, shown and summarized as one task."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import ctypes
|
|
5
|
+
import ctypes.util
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .api_errors import APIError
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger("mobster.fanout")
|
|
19
|
+
|
|
20
|
+
MAX_PHONES = 100
|
|
21
|
+
SOFT_LIMIT = 10
|
|
22
|
+
CPU_HOLD = 85.0
|
|
23
|
+
HOLD_MAX_S = 300.0
|
|
24
|
+
HOLD_POLL_S = 1.0
|
|
25
|
+
HOLD_STAGGER_S = 1.0
|
|
26
|
+
KEEP = 200
|
|
27
|
+
GROUP_ID = re.compile(r"[a-f0-9]{12}")
|
|
28
|
+
STORE = "groups.json"
|
|
29
|
+
DONE = frozenset({"completed"})
|
|
30
|
+
|
|
31
|
+
HELD = "Your Mac is busy (CPU {cpu:.0f}%). Mobster starts the other phones when it drops below {limit:.0f}%."
|
|
32
|
+
GAVE_UP = "Your Mac stayed busy, so this phone didn't start. Nothing ran on it. Try again when the Mac is less busy."
|
|
33
|
+
SOFT = ("{count} tasks at once is more than {limit}. Each phone streams its screen and sends screenshots, so a busy "
|
|
34
|
+
"Mac or USB hub can slow every task down.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MacLoad:
|
|
38
|
+
MIN_GAP = 0.5
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self.lock = threading.Lock()
|
|
42
|
+
self.last = None
|
|
43
|
+
self.value = None
|
|
44
|
+
self._host = self._libc = None
|
|
45
|
+
try:
|
|
46
|
+
libc = ctypes.CDLL(ctypes.util.find_library("System") or "/usr/lib/libSystem.B.dylib")
|
|
47
|
+
libc.mach_host_self.restype = ctypes.c_uint
|
|
48
|
+
self._host, self._libc = libc.mach_host_self(), libc
|
|
49
|
+
except (OSError, AttributeError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def ticks(self):
|
|
53
|
+
if self._libc is not None:
|
|
54
|
+
info = (ctypes.c_uint * 4)() # user, system, idle, nice (HOST_CPU_LOAD_INFO)
|
|
55
|
+
count = ctypes.c_uint(4)
|
|
56
|
+
if self._libc.host_statistics(self._host, 3, ctypes.byref(info), ctypes.byref(count)) == 0:
|
|
57
|
+
user, system, idle, nice = list(info)
|
|
58
|
+
return user + system + nice, user + system + idle + nice
|
|
59
|
+
try:
|
|
60
|
+
with open("/proc/stat") as stream:
|
|
61
|
+
values = [int(part) for part in stream.readline().split()[1:8]]
|
|
62
|
+
idle = values[3] + values[4]
|
|
63
|
+
return sum(values) - idle, sum(values)
|
|
64
|
+
except (OSError, ValueError, IndexError):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def percent(self):
|
|
68
|
+
with self.lock:
|
|
69
|
+
now = time.monotonic()
|
|
70
|
+
if self.last is not None and now - self.last[0] < self.MIN_GAP and self.value is not None:
|
|
71
|
+
return self.value
|
|
72
|
+
ticks = self.ticks()
|
|
73
|
+
if ticks is None:
|
|
74
|
+
try:
|
|
75
|
+
self.value = min(100.0, os.getloadavg()[0] / (os.cpu_count() or 1) * 100)
|
|
76
|
+
except OSError:
|
|
77
|
+
self.value = 0.0
|
|
78
|
+
return self.value
|
|
79
|
+
if self.last is None:
|
|
80
|
+
# The first read has nothing to compare with: the load average stands in.
|
|
81
|
+
self.last = (now, ticks)
|
|
82
|
+
try:
|
|
83
|
+
self.value = round(min(100.0, os.getloadavg()[0] / (os.cpu_count() or 1) * 100), 1)
|
|
84
|
+
except OSError:
|
|
85
|
+
self.value = 0.0
|
|
86
|
+
return self.value
|
|
87
|
+
first = self.last[1]
|
|
88
|
+
busy = (ticks[0] - first[0]) % 2 ** 32 if ticks[0] < first[0] else ticks[0] - first[0]
|
|
89
|
+
total = (ticks[1] - first[1]) % 2 ** 32 if ticks[1] < first[1] else ticks[1] - first[1]
|
|
90
|
+
if total > 0:
|
|
91
|
+
self.value = round(100.0 * busy / total, 1)
|
|
92
|
+
elif self.value is None:
|
|
93
|
+
self.value = 0.0
|
|
94
|
+
self.last = (time.monotonic(), ticks)
|
|
95
|
+
return self.value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
BULLET = re.compile(r"^\s*(?:[-*•·–]|\d{1,3}[.)])\s+")
|
|
99
|
+
TRIM = re.compile(r"^[\W_]+|[\W_]+$", re.UNICODE)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _key(line):
|
|
103
|
+
return TRIM.sub("", " ".join(BULLET.sub("", line).split()).casefold())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def answer_of(summary):
|
|
107
|
+
summary = summary if isinstance(summary, dict) else {}
|
|
108
|
+
data = summary.get("data")
|
|
109
|
+
if isinstance(data, str) and data.strip():
|
|
110
|
+
return data.strip()
|
|
111
|
+
full = summary.get("full_answer") or summary.get("answer")
|
|
112
|
+
return full.strip() if isinstance(full, str) and full.strip() else None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def merge_results(results):
|
|
116
|
+
answers, lines, order, rows, seen_rows, tables = [], {}, [], [], set(), True
|
|
117
|
+
completed, cost, priced = 0, 0.0, False
|
|
118
|
+
for item in results:
|
|
119
|
+
summary = item.get("summary") if isinstance(item.get("summary"), dict) else {}
|
|
120
|
+
status = item.get("status") or summary.get("status") or "error"
|
|
121
|
+
text = answer_of(summary)
|
|
122
|
+
answers.append({"device": item.get("device"), "deviceName": item.get("deviceName"),
|
|
123
|
+
**({"profile": item["profile"], "profileName": item.get("profileName")}
|
|
124
|
+
if item.get("profile") else {}), "status": status,
|
|
125
|
+
"answer": text[:3000] if text else None,
|
|
126
|
+
"reason": None if text or status in DONE else (str(summary.get("reason") or "")[:500] or None)})
|
|
127
|
+
if isinstance(item.get("costUsd"), (int, float)):
|
|
128
|
+
cost, priced = cost + float(item["costUsd"]), True
|
|
129
|
+
if status not in DONE:
|
|
130
|
+
continue
|
|
131
|
+
completed += 1
|
|
132
|
+
data = summary.get("data")
|
|
133
|
+
if isinstance(data, list) and all(isinstance(row, dict) for row in data):
|
|
134
|
+
for row in data:
|
|
135
|
+
key = json.dumps(row, sort_keys=True, ensure_ascii=False, default=str)
|
|
136
|
+
if key not in seen_rows:
|
|
137
|
+
seen_rows.add(key)
|
|
138
|
+
rows.append(row)
|
|
139
|
+
else:
|
|
140
|
+
tables = False
|
|
141
|
+
for line in (text or "").splitlines():
|
|
142
|
+
key = _key(line)
|
|
143
|
+
# A heading ("Your notes:") introduces each phone's list; the lines under it are what they found.
|
|
144
|
+
if not key or line.rstrip().endswith(":"):
|
|
145
|
+
continue
|
|
146
|
+
if key not in lines:
|
|
147
|
+
lines[key] = {"text": BULLET.sub("", line).strip(), "phones": []}
|
|
148
|
+
order.append(key)
|
|
149
|
+
if item.get("device") not in lines[key]["phones"]:
|
|
150
|
+
lines[key]["phones"].append(item.get("device"))
|
|
151
|
+
return {"phones": len(results), "completed": completed, "answers": answers,
|
|
152
|
+
"lines": [lines[key] for key in order][:400], "rows": rows[:1000] if tables and completed else None,
|
|
153
|
+
"costUsd": round(cost, 6) if priced else None}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
PART_STATUS = re.compile(r"[a-z_]{1,32}")
|
|
157
|
+
MAX_PART_ROWS = 1000
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def merge_parts(parts):
|
|
161
|
+
"""One summary for a task run as one group on each of several Macs (the console's fan-out across Macs): ``parts``
|
|
162
|
+
are the groups' summaries as the console shows them, phones by team id ({answers: [{device, deviceName, status,
|
|
163
|
+
answer, reason}], rows, costUsd}). The answers are merged as ``merge_results`` merges one Mac's phones; the rows
|
|
164
|
+
are every part's once, when every part has rows; the cost is the sum of the parts' known costs."""
|
|
165
|
+
results, rows, seen, tables, cost, priced = [], [], set(), True, 0.0, False
|
|
166
|
+
for part in parts:
|
|
167
|
+
for item in part["answers"]:
|
|
168
|
+
summary = {"status": item["status"], "answer": item.get("answer"), "reason": item.get("reason")}
|
|
169
|
+
results.append({"device": item["device"], "deviceName": item.get("deviceName"), "status": item["status"],
|
|
170
|
+
"summary": summary})
|
|
171
|
+
if isinstance(part.get("rows"), list):
|
|
172
|
+
for row in part["rows"]:
|
|
173
|
+
key = json.dumps(row, sort_keys=True, ensure_ascii=False, default=str)
|
|
174
|
+
if key not in seen:
|
|
175
|
+
seen.add(key)
|
|
176
|
+
rows.append(row)
|
|
177
|
+
else:
|
|
178
|
+
tables = False
|
|
179
|
+
if isinstance(part.get("costUsd"), (int, float)):
|
|
180
|
+
cost, priced = cost + float(part["costUsd"]), True
|
|
181
|
+
merged = merge_results(results)
|
|
182
|
+
merged["rows"] = rows[:MAX_PART_ROWS] if tables and merged["completed"] else None
|
|
183
|
+
merged["costUsd"] = round(cost, 6) if priced else None
|
|
184
|
+
return merged
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def new_id():
|
|
188
|
+
return uuid.uuid4().hex[:12]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def retained_groups(groups):
|
|
192
|
+
finished = [group for group in groups if group.get("finishedAt") is not None]
|
|
193
|
+
kept = {group["id"] for group in finished[-KEEP:]}
|
|
194
|
+
return [group for group in groups if group.get("finishedAt") is None or group["id"] in kept]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class GroupStore:
|
|
198
|
+
def __init__(self, folder):
|
|
199
|
+
self.path = Path(folder) / STORE if folder else None
|
|
200
|
+
|
|
201
|
+
def read(self):
|
|
202
|
+
if self.path is None:
|
|
203
|
+
return []
|
|
204
|
+
try:
|
|
205
|
+
data = json.loads(self.path.read_text())
|
|
206
|
+
except (OSError, ValueError):
|
|
207
|
+
return []
|
|
208
|
+
groups = data.get("groups") if isinstance(data, dict) else None
|
|
209
|
+
return [group for group in groups or [] if isinstance(group, dict)
|
|
210
|
+
and isinstance(group.get("id"), str) and GROUP_ID.fullmatch(group["id"])
|
|
211
|
+
and isinstance(group.get("members"), list)]
|
|
212
|
+
|
|
213
|
+
def write(self, groups):
|
|
214
|
+
if self.path is None:
|
|
215
|
+
return
|
|
216
|
+
try:
|
|
217
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
fd, temporary = tempfile.mkstemp(prefix=".groups.", suffix=".json", dir=self.path.parent)
|
|
219
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
220
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
221
|
+
stream.write(json.dumps({"version": 1, "groups": retained_groups(groups)}, ensure_ascii=False))
|
|
222
|
+
os.replace(temporary, self.path)
|
|
223
|
+
except OSError:
|
|
224
|
+
log.warning("mobster: could not save the task groups")
|
|
225
|
+
with contextlib.suppress(OSError, UnboundLocalError):
|
|
226
|
+
os.unlink(temporary)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Fanout:
|
|
230
|
+
def __init__(self, runtime, load=None):
|
|
231
|
+
self.runtime = runtime
|
|
232
|
+
state_db = getattr(runtime.config, "state_db", None)
|
|
233
|
+
self.store = GroupStore(Path(state_db).parent if isinstance(state_db, str) and state_db else None)
|
|
234
|
+
self.lock = threading.RLock()
|
|
235
|
+
self.groups = {group["id"]: group for group in self.store.read()}
|
|
236
|
+
self.keys = {group["key"]: group["id"] for group in self.groups.values() if group.get("key")}
|
|
237
|
+
self.load = load or MacLoad()
|
|
238
|
+
self.wake = threading.Event()
|
|
239
|
+
self.dispatcher = None
|
|
240
|
+
self.closed = False
|
|
241
|
+
# Phones held by a group that was interrupted by a restart never start later: say so.
|
|
242
|
+
for group in list(self.groups.values()):
|
|
243
|
+
for member in group["members"]:
|
|
244
|
+
if member.get("state") in ("held", "pending"):
|
|
245
|
+
member.update(state="refused", code="interrupted",
|
|
246
|
+
error="Mobster restarted before this phone's task was confirmed. "
|
|
247
|
+
"Inspect the phone before starting another task; it was not retried.")
|
|
248
|
+
self.finish_if_done(group, notify=False)
|
|
249
|
+
|
|
250
|
+
def running(self):
|
|
251
|
+
with self.runtime.lock:
|
|
252
|
+
return len(self.runtime.active_runs)
|
|
253
|
+
|
|
254
|
+
def limits(self):
|
|
255
|
+
cpu = self.load.percent()
|
|
256
|
+
return {"running": self.running(), "softLimit": SOFT_LIMIT, "cpuPercent": cpu, "holdAbovePercent": CPU_HOLD,
|
|
257
|
+
"holding": cpu > CPU_HOLD, "maxPhones": MAX_PHONES}
|
|
258
|
+
|
|
259
|
+
def get(self, group_id):
|
|
260
|
+
with self.lock:
|
|
261
|
+
group = self.groups.get(group_id)
|
|
262
|
+
if group is None:
|
|
263
|
+
raise APIError("That task group doesn't exist here.", 404, "group_not_found")
|
|
264
|
+
return group
|
|
265
|
+
|
|
266
|
+
def has_request(self, key):
|
|
267
|
+
with self.lock:
|
|
268
|
+
return key in self.keys
|
|
269
|
+
|
|
270
|
+
def public(self, group):
|
|
271
|
+
runs = self.runtime.runs
|
|
272
|
+
members, counts = [], {"phones": 0, "running": 0, "waiting": 0, "completed": 0, "ended": 0, "refused": 0,
|
|
273
|
+
"held": 0}
|
|
274
|
+
for member in group["members"]:
|
|
275
|
+
run = runs.get(member.get("runId")) if member.get("runId") else None
|
|
276
|
+
entry = {key: member.get(key) for key in ("device", "deviceName", "runId", "state", "error", "code")}
|
|
277
|
+
if member.get("profile"):
|
|
278
|
+
entry.update(profile=member["profile"], profileName=member.get("profileName"))
|
|
279
|
+
approval = None
|
|
280
|
+
if run is not None:
|
|
281
|
+
with run.condition:
|
|
282
|
+
approval = dict(run.approval) if run.approval else None
|
|
283
|
+
entry.update(status=run.status, finishedAt=run.finished_at * 1000 if run.finished_at else None,
|
|
284
|
+
costUsd=run.cost_usd, approval=approval, estimateUsd=run.estimate_usd)
|
|
285
|
+
else:
|
|
286
|
+
entry.update(status=None, finishedAt=None, costUsd=member.get("costUsd"), approval=None,
|
|
287
|
+
estimateUsd=None)
|
|
288
|
+
counts["phones"] += 1
|
|
289
|
+
if member["state"] == "refused":
|
|
290
|
+
counts["refused"] += 1
|
|
291
|
+
elif member["state"] == "held":
|
|
292
|
+
counts["held"] += 1
|
|
293
|
+
elif run is not None and run.finished_at is None:
|
|
294
|
+
counts["waiting" if approval else "running"] += 1
|
|
295
|
+
elif run is not None and run.status in DONE or member.get("status") in DONE:
|
|
296
|
+
counts["completed"] += 1
|
|
297
|
+
else:
|
|
298
|
+
counts["ended"] += 1
|
|
299
|
+
members.append(entry)
|
|
300
|
+
state = ("waiting" if counts["waiting"] else "running" if counts["running"] else "held" if counts["held"]
|
|
301
|
+
else "finished" if group.get("finishedAt") is not None else "running")
|
|
302
|
+
estimates = [item["estimateUsd"] for item in members if isinstance(item.get("estimateUsd"), (int, float))]
|
|
303
|
+
estimate = group.get("estimateUsd") if group.get("estimateUsd") is not None else (
|
|
304
|
+
max(estimates) if estimates else None)
|
|
305
|
+
started = sum(1 for item in members if item["state"] != "refused")
|
|
306
|
+
return {"id": group["id"], "goal": group["goal"], "appId": group.get("appId"), "engine": group.get("engine"),
|
|
307
|
+
"origin": group.get("origin"), "threadId": group.get("threadId"), "createdAt": group["createdAt"],
|
|
308
|
+
"finishedAt": group.get("finishedAt"), "state": state, "counts": counts, "members": members,
|
|
309
|
+
"warning": group.get("warning"), "held": group.get("held"),
|
|
310
|
+
"estimateUsd": estimate,
|
|
311
|
+
"estimateTotalUsd": round(estimate * started, 6) if estimate is not None else None,
|
|
312
|
+
"summary": group.get("summary"),
|
|
313
|
+
**({"teamGroup": group["teamGroup"]} if group.get("teamGroup") else {})}
|
|
314
|
+
|
|
315
|
+
def recent(self, limit=20):
|
|
316
|
+
with self.lock:
|
|
317
|
+
groups = sorted(self.groups.values(), key=lambda group: group["createdAt"], reverse=True)[:limit]
|
|
318
|
+
return [self.public(group) for group in groups]
|
|
319
|
+
|
|
320
|
+
def resolve(self, phones):
|
|
321
|
+
if not isinstance(phones, list) or not 1 <= len(phones) <= MAX_PHONES:
|
|
322
|
+
raise ValueError(f"phones is a list of 1 to {MAX_PHONES} devices (their ids, UDIDs or names)")
|
|
323
|
+
if not all(isinstance(item, str) and 1 <= len(item.strip()) <= 128 for item in phones):
|
|
324
|
+
raise ValueError("Name each phone by its id, UDID or name")
|
|
325
|
+
fleet = getattr(self.runtime, "fleet", None)
|
|
326
|
+
if fleet is None or getattr(self.runtime, "pool", None) is not None:
|
|
327
|
+
raise ValueError("This Mobster runs every task on its own device; leave phones out")
|
|
328
|
+
resolved, seen = [], set()
|
|
329
|
+
for name in phones:
|
|
330
|
+
phone = self.runtime.resolve_device(name)
|
|
331
|
+
if phone.id in seen:
|
|
332
|
+
continue
|
|
333
|
+
seen.add(phone.id)
|
|
334
|
+
resolved.append(phone)
|
|
335
|
+
return resolved
|
|
336
|
+
|
|
337
|
+
def start(self, phones, app_id, goal, *, idempotency_key=None, fingerprint=None, thread_id=None, team_group=None,
|
|
338
|
+
**create):
|
|
339
|
+
"""Start ``goal`` on every phone of ``phones``: (group, replayed). ``team_group``: the console's id for the
|
|
340
|
+
same task on its other Macs' phones (12 hex), kept on the group."""
|
|
341
|
+
if team_group is not None and (not isinstance(team_group, str) or not GROUP_ID.fullmatch(team_group)):
|
|
342
|
+
raise ValueError("teamGroup is 12 lowercase hex characters")
|
|
343
|
+
resolved = self.resolve(phones)
|
|
344
|
+
members = [{"device": phone.id, "deviceName": phone.name, "runId": None, "state": "pending",
|
|
345
|
+
"error": None, "code": None} for phone in resolved]
|
|
346
|
+
return self._open(members, app_id, goal, idempotency_key, fingerprint, thread_id, create, team_group=team_group)
|
|
347
|
+
|
|
348
|
+
def start_profiles(self, profiles, app_id, goal, *, idempotency_key=None, fingerprint=None, thread_id=None,
|
|
349
|
+
**create):
|
|
350
|
+
# Profiles that share a phone run in turn. Nothing is retried: an unknown outcome is that profile's result.
|
|
351
|
+
resolved = self.resolve_profiles(profiles)
|
|
352
|
+
members, first_on = [], {}
|
|
353
|
+
for account in resolved:
|
|
354
|
+
device = account.get("device")
|
|
355
|
+
member = {"device": device, "deviceName": account.get("deviceName"), "profile": account["id"],
|
|
356
|
+
"profileName": account.get("handle"), "runId": None, "state": "pending", "error": None,
|
|
357
|
+
"code": None}
|
|
358
|
+
if not device:
|
|
359
|
+
member.update(state="refused", code="no_phone",
|
|
360
|
+
error=f"@{str(account.get('handle') or '').lstrip('@')} "
|
|
361
|
+
"has no phone yet. Choose its phone on the Profiles page, then run it again.")
|
|
362
|
+
elif device in first_on:
|
|
363
|
+
member["state"] = "queued" # starts when the profile ahead of it on this phone has finished
|
|
364
|
+
else:
|
|
365
|
+
first_on[device] = member
|
|
366
|
+
members.append(member)
|
|
367
|
+
return self._open(members, app_id, goal, idempotency_key, fingerprint, thread_id, create)
|
|
368
|
+
|
|
369
|
+
def resolve_profiles(self, profiles):
|
|
370
|
+
from .accounts.store import AccountNotFound, AmbiguousAccount, resolve, store_for
|
|
371
|
+
stored = store_for(self.runtime).all()
|
|
372
|
+
if profiles == "all":
|
|
373
|
+
chosen = list(stored)
|
|
374
|
+
else:
|
|
375
|
+
if not isinstance(profiles, list) or not 1 <= len(profiles) <= MAX_PHONES or not all(
|
|
376
|
+
isinstance(item, str) and 1 <= len(item.strip()) <= 128 for item in profiles):
|
|
377
|
+
raise ValueError(f"profiles is \"all\", or a list of 1 to {MAX_PHONES} profiles (their ids or handles)")
|
|
378
|
+
chosen = []
|
|
379
|
+
for name in profiles:
|
|
380
|
+
try:
|
|
381
|
+
chosen.append(resolve(stored, name))
|
|
382
|
+
except (AccountNotFound, AmbiguousAccount) as error:
|
|
383
|
+
raise ValueError(str(error)) from None
|
|
384
|
+
seen, out = set(), []
|
|
385
|
+
for account in chosen:
|
|
386
|
+
if account["id"] not in seen:
|
|
387
|
+
seen.add(account["id"])
|
|
388
|
+
out.append(account)
|
|
389
|
+
if not out:
|
|
390
|
+
raise ValueError("There are no profiles yet. Add one on the Profiles page, then run it on every profile.")
|
|
391
|
+
return out[:MAX_PHONES]
|
|
392
|
+
|
|
393
|
+
def _open(self, members, app_id, goal, idempotency_key, fingerprint, thread_id, create, *, team_group=None):
|
|
394
|
+
if not isinstance(goal, str) or not 1 <= len(goal.strip()) <= 4000:
|
|
395
|
+
raise ValueError("Choose an app, a task (1–4000 characters), and demo/live mode")
|
|
396
|
+
resolved = members
|
|
397
|
+
with self.lock:
|
|
398
|
+
if idempotency_key:
|
|
399
|
+
prior = self.keys.get(idempotency_key)
|
|
400
|
+
if prior is not None and prior in self.groups:
|
|
401
|
+
if self.groups[prior].get("fingerprint") != fingerprint:
|
|
402
|
+
raise APIError("This request key belongs to a different task", code="idempotency_conflict")
|
|
403
|
+
return self.groups[prior], True
|
|
404
|
+
running = self.running()
|
|
405
|
+
group = {"id": new_id(), "key": idempotency_key, "fingerprint": fingerprint, "goal": goal.strip(),
|
|
406
|
+
"appId": app_id, "engine": create.get("engine"), "origin": create.get("origin", "api"),
|
|
407
|
+
"threadId": thread_id, "createdAt": time.time() * 1000, "finishedAt": None, "summary": None,
|
|
408
|
+
**({"teamGroup": team_group} if team_group else {}),
|
|
409
|
+
"warning": SOFT.format(count=running + len(resolved), limit=SOFT_LIMIT)
|
|
410
|
+
if running + len(resolved) > SOFT_LIMIT else None, "held": None, "estimateUsd": None,
|
|
411
|
+
"members": members,
|
|
412
|
+
"create": {"app_id": app_id, "goal": goal.strip(), **{key: value for key, value in create.items()
|
|
413
|
+
if key in RUN_KEYS}}}
|
|
414
|
+
self.groups[group["id"]] = group
|
|
415
|
+
if idempotency_key:
|
|
416
|
+
self.keys[idempotency_key] = group["id"]
|
|
417
|
+
cpu = self.load.percent()
|
|
418
|
+
if cpu > CPU_HOLD:
|
|
419
|
+
with self.lock:
|
|
420
|
+
for member in group["members"]:
|
|
421
|
+
if member["state"] == "pending":
|
|
422
|
+
member["state"] = "held"
|
|
423
|
+
if any(member["state"] == "held" for member in group["members"]):
|
|
424
|
+
group["held"] = {"cpuPercent": cpu, "since": time.time() * 1000,
|
|
425
|
+
"reason": HELD.format(cpu=cpu, limit=CPU_HOLD)}
|
|
426
|
+
else:
|
|
427
|
+
for member in group["members"]:
|
|
428
|
+
self._admit(group, member)
|
|
429
|
+
with self.lock:
|
|
430
|
+
self.save()
|
|
431
|
+
if not any(member["state"] in ("started", "held", "queued") for member in group["members"]):
|
|
432
|
+
# Nothing started: fail as a one-phone start would, with the first refusal.
|
|
433
|
+
with self.lock:
|
|
434
|
+
self.groups.pop(group["id"], None)
|
|
435
|
+
if idempotency_key:
|
|
436
|
+
self.keys.pop(idempotency_key, None)
|
|
437
|
+
self.save()
|
|
438
|
+
first = group["members"][0]
|
|
439
|
+
raise APIError(first.get("error") or "None of those phones could start this task.", 409,
|
|
440
|
+
first.get("code") or "group_refused",
|
|
441
|
+
refused=[{key: member.get(key) for key in ("device", "deviceName", "error", "code")}
|
|
442
|
+
for member in group["members"]])
|
|
443
|
+
self.notify(group, "created")
|
|
444
|
+
if group["held"] is not None:
|
|
445
|
+
self._dispatch()
|
|
446
|
+
return group, False
|
|
447
|
+
|
|
448
|
+
def _admit_queued(self, group, member):
|
|
449
|
+
with self.lock:
|
|
450
|
+
if member["state"] != "queued":
|
|
451
|
+
return
|
|
452
|
+
member["state"] = "pending"
|
|
453
|
+
self._admit(group, member)
|
|
454
|
+
with self.lock:
|
|
455
|
+
self.save()
|
|
456
|
+
self.notify(group, "updated")
|
|
457
|
+
|
|
458
|
+
def _admit(self, group, member):
|
|
459
|
+
# Stop uses this same lock: it either refuses this member before admission or sees its new run id.
|
|
460
|
+
with self.lock:
|
|
461
|
+
if self.closed or member["state"] not in ("pending", "held"):
|
|
462
|
+
return None
|
|
463
|
+
create = dict(group["create"])
|
|
464
|
+
if member.get("profile"):
|
|
465
|
+
# A task as this profile: its own phone, the profile confirmed on screen before it commits.
|
|
466
|
+
create["extras"] = {**(create.get("extras") or {}), "account": member["profile"]}
|
|
467
|
+
create["engine"] = create.get("engine") or "smart"
|
|
468
|
+
try:
|
|
469
|
+
run = self.runtime.create(create.pop("app_id"), create.pop("goal"), "live", device=member["device"],
|
|
470
|
+
group_id=group["id"], **create)
|
|
471
|
+
except APIError as error:
|
|
472
|
+
member.update(state="refused", error=str(error), code=error.code)
|
|
473
|
+
return None
|
|
474
|
+
except ValueError as error:
|
|
475
|
+
member.update(state="refused", error=str(error), code="invalid")
|
|
476
|
+
return None
|
|
477
|
+
run = run[0] if isinstance(run, tuple) else run
|
|
478
|
+
member.update(state="started", runId=run.id, deviceName=run.device_name or member.get("deviceName"),
|
|
479
|
+
error=None, code=None)
|
|
480
|
+
if group.get("estimateUsd") is None and run.estimate_usd is not None:
|
|
481
|
+
group["estimateUsd"] = run.estimate_usd
|
|
482
|
+
return run
|
|
483
|
+
|
|
484
|
+
def _dispatch(self):
|
|
485
|
+
self.wake.set()
|
|
486
|
+
with self.lock:
|
|
487
|
+
if self.dispatcher is not None and self.dispatcher.is_alive():
|
|
488
|
+
return
|
|
489
|
+
self.dispatcher = threading.Thread(target=self._held_loop, name="mobster-fanout-held", daemon=True)
|
|
490
|
+
self.dispatcher.start()
|
|
491
|
+
|
|
492
|
+
def _held_loop(self):
|
|
493
|
+
while not self.closed:
|
|
494
|
+
with self.lock:
|
|
495
|
+
waiting = [(group, member) for group in self.groups.values() for member in group["members"]
|
|
496
|
+
if member["state"] == "held"]
|
|
497
|
+
if not waiting:
|
|
498
|
+
self.dispatcher = None
|
|
499
|
+
return
|
|
500
|
+
group, member = waiting[0]
|
|
501
|
+
held = group.get("held") or {}
|
|
502
|
+
since = held.get("since") or time.time() * 1000
|
|
503
|
+
if time.time() * 1000 - since > HOLD_MAX_S * 1000:
|
|
504
|
+
with self.lock:
|
|
505
|
+
if member["state"] == "held":
|
|
506
|
+
member.update(state="refused", code="mac_busy", error=GAVE_UP)
|
|
507
|
+
self._settle(group)
|
|
508
|
+
continue
|
|
509
|
+
cpu = self.load.percent()
|
|
510
|
+
if cpu <= CPU_HOLD and not getattr(self.runtime, "closing", False):
|
|
511
|
+
self._admit(group, member)
|
|
512
|
+
self._settle(group)
|
|
513
|
+
# The next phone waits a moment, so the CPU read before it sees this one's start.
|
|
514
|
+
self.wake.clear()
|
|
515
|
+
self.wake.wait(HOLD_STAGGER_S)
|
|
516
|
+
continue
|
|
517
|
+
with self.lock:
|
|
518
|
+
if group.get("held") is not None:
|
|
519
|
+
group["held"].update(cpuPercent=cpu, reason=HELD.format(cpu=cpu, limit=CPU_HOLD))
|
|
520
|
+
self.wake.clear()
|
|
521
|
+
self.wake.wait(HOLD_POLL_S)
|
|
522
|
+
|
|
523
|
+
def _settle(self, group):
|
|
524
|
+
with self.lock:
|
|
525
|
+
if not any(member["state"] == "held" for member in group["members"]):
|
|
526
|
+
group["held"] = None
|
|
527
|
+
self.save()
|
|
528
|
+
self.notify(group, "updated")
|
|
529
|
+
self.finish_if_done(group)
|
|
530
|
+
|
|
531
|
+
def stop(self, group_id):
|
|
532
|
+
group = self.get(group_id)
|
|
533
|
+
with self.lock:
|
|
534
|
+
for member in group["members"]:
|
|
535
|
+
if member["state"] in ("held", "pending", "queued"):
|
|
536
|
+
member.update(state="refused", code="stopped", error="Stopped before it started on this phone.")
|
|
537
|
+
for member in group["members"]:
|
|
538
|
+
if member.get("runId") and member["runId"] in self.runtime.runs:
|
|
539
|
+
with contextlib.suppress(APIError):
|
|
540
|
+
self.runtime.stop_run(member["runId"])
|
|
541
|
+
with self.lock:
|
|
542
|
+
group["held"] = None
|
|
543
|
+
self.save()
|
|
544
|
+
self.notify(group, "updated")
|
|
545
|
+
self.finish_if_done(group)
|
|
546
|
+
return group
|
|
547
|
+
|
|
548
|
+
def answer_approvals(self, group_id, approve, approvals):
|
|
549
|
+
# Only an approval still waiting with exactly the id the person saw is answered.
|
|
550
|
+
group = self.get(group_id)
|
|
551
|
+
if type(approve) is not bool:
|
|
552
|
+
raise ValueError("approve is true or false")
|
|
553
|
+
if not isinstance(approvals, dict) or not 1 <= len(approvals) <= MAX_PHONES:
|
|
554
|
+
raise ValueError("approvals maps each phone's run id to the approval id that was shown")
|
|
555
|
+
members = {member.get("runId"): member for member in group["members"] if member.get("runId")}
|
|
556
|
+
requests = []
|
|
557
|
+
for run_id, approval_id in approvals.items():
|
|
558
|
+
if run_id not in members or not isinstance(approval_id, str):
|
|
559
|
+
raise ValueError("Each approval belongs to one of this group's runs")
|
|
560
|
+
run = self.runtime.runs.get(run_id)
|
|
561
|
+
if run is not None:
|
|
562
|
+
with run.condition:
|
|
563
|
+
pending = dict(run.approval) if run.approval else None
|
|
564
|
+
if pending is not None and pending.get("id") == approval_id and pending.get("kind") == "clarify":
|
|
565
|
+
raise ValueError("Mobster's questions are answered one at a time, on their phone's tab")
|
|
566
|
+
requests.append((run_id, approval_id, run))
|
|
567
|
+
# Validate the whole request before answering any card: a later invalid entry must not approve an earlier one.
|
|
568
|
+
out = []
|
|
569
|
+
for run_id, approval_id, run in requests:
|
|
570
|
+
answered = bool(run is not None and run.answer_approval(approval_id, approve))
|
|
571
|
+
out.append({"runId": run_id, "device": members[run_id].get("device"), "answered": answered,
|
|
572
|
+
"decision": None if answered or run is None else run.approval_decision(approval_id)})
|
|
573
|
+
return out
|
|
574
|
+
|
|
575
|
+
def member_finished(self, run):
|
|
576
|
+
group_id = getattr(run, "group_id", None)
|
|
577
|
+
if not group_id:
|
|
578
|
+
return
|
|
579
|
+
with self.lock:
|
|
580
|
+
group = self.groups.get(group_id)
|
|
581
|
+
if group is None:
|
|
582
|
+
return
|
|
583
|
+
following = None
|
|
584
|
+
for member in group["members"]:
|
|
585
|
+
if member.get("runId") == run.id:
|
|
586
|
+
member.update(status=run.status, costUsd=run.cost_usd)
|
|
587
|
+
following = next((other for other in group["members"] if other["state"] == "queued"
|
|
588
|
+
and other.get("device") == member.get("device")), None)
|
|
589
|
+
self.save()
|
|
590
|
+
if following is not None:
|
|
591
|
+
self._admit_queued(group, following)
|
|
592
|
+
if not self.finish_if_done(group):
|
|
593
|
+
self.notify(group, "updated")
|
|
594
|
+
|
|
595
|
+
def finish_if_done(self, group, *, notify=True):
|
|
596
|
+
runs = self.runtime.runs
|
|
597
|
+
with self.lock:
|
|
598
|
+
if group.get("finishedAt") is not None:
|
|
599
|
+
return False
|
|
600
|
+
for member in group["members"]:
|
|
601
|
+
if member["state"] in ("held", "pending", "queued"):
|
|
602
|
+
return False
|
|
603
|
+
run = runs.get(member.get("runId")) if member.get("runId") else None
|
|
604
|
+
if run is not None and run.finished_at is None:
|
|
605
|
+
return False
|
|
606
|
+
results = []
|
|
607
|
+
for member in group["members"]:
|
|
608
|
+
run = runs.get(member.get("runId")) if member.get("runId") else None
|
|
609
|
+
results.append({"device": member["device"], "deviceName": member.get("deviceName"),
|
|
610
|
+
"profile": member.get("profile"), "profileName": member.get("profileName"),
|
|
611
|
+
"status": run.status if run is not None else ("refused" if member["state"] == "refused"
|
|
612
|
+
else member.get("status")),
|
|
613
|
+
"summary": (run.summary if run is not None else None) or (
|
|
614
|
+
{"status": "refused", "reason": member.get("error")}
|
|
615
|
+
if member["state"] == "refused" else {}),
|
|
616
|
+
"costUsd": run.cost_usd if run is not None else member.get("costUsd")})
|
|
617
|
+
group["summary"] = merge_results(results)
|
|
618
|
+
group["finishedAt"] = time.time() * 1000
|
|
619
|
+
self.save()
|
|
620
|
+
if notify:
|
|
621
|
+
self.notify(group, "finished")
|
|
622
|
+
return True
|
|
623
|
+
|
|
624
|
+
def notify(self, group, event):
|
|
625
|
+
self.runtime.publish("groups", {"event": f"group_{event}", "groupId": group["id"],
|
|
626
|
+
**({"threadId": group["threadId"]} if group.get("threadId") else {})})
|
|
627
|
+
public = self.public(group)
|
|
628
|
+
for listener in list(LISTENERS):
|
|
629
|
+
try:
|
|
630
|
+
listener(self.runtime, public, event)
|
|
631
|
+
except Exception: # noqa: BLE001 -- a listener never changes the group
|
|
632
|
+
log.exception("a group listener failed")
|
|
633
|
+
|
|
634
|
+
def save(self):
|
|
635
|
+
# Call with ``lock`` held. A held run's ``create`` options stay in memory only.
|
|
636
|
+
ordered = sorted(self.groups.values(), key=lambda group: group["createdAt"])
|
|
637
|
+
retained = retained_groups(ordered)
|
|
638
|
+
if len(retained) < len(ordered):
|
|
639
|
+
kept = {group["id"] for group in retained}
|
|
640
|
+
for old in ordered:
|
|
641
|
+
if old["id"] in kept:
|
|
642
|
+
continue
|
|
643
|
+
self.groups.pop(old["id"], None)
|
|
644
|
+
if old.get("key"):
|
|
645
|
+
self.keys.pop(old["key"], None)
|
|
646
|
+
self.store.write([{key: value for key, value in group.items() if key != "create"} for group in retained])
|
|
647
|
+
|
|
648
|
+
def close(self):
|
|
649
|
+
self.closed = True
|
|
650
|
+
self.wake.set()
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
RUN_KEYS = frozenset({"output_schema", "output_format", "allowed_bundles", "engine", "extras", "origin"})
|
|
654
|
+
|
|
655
|
+
# Called with (runtime, public group, event) on "created", "updated" and "finished".
|
|
656
|
+
LISTENERS = []
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def add_listener(fn):
|
|
660
|
+
if not callable(fn):
|
|
661
|
+
raise ValueError("A group listener is a function")
|
|
662
|
+
if fn not in LISTENERS:
|
|
663
|
+
LISTENERS.append(fn)
|