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,404 @@
|
|
|
1
|
+
"""App API sessions: a phone leased to one client for a while, watched live, recorded, and kept afterwards."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import reading
|
|
13
|
+
|
|
14
|
+
ID_HEX = 12
|
|
15
|
+
DEFAULT_TIMEOUT, MAX_TIMEOUT = 300, 3600
|
|
16
|
+
DEFAULT_IDLE, MAX_IDLE = 120, 3600
|
|
17
|
+
RECORD_FPS = 2.0
|
|
18
|
+
MAX_FRAMES = 3600
|
|
19
|
+
MAX_EVENTS = 2000
|
|
20
|
+
KEPT_SESSIONS = 50
|
|
21
|
+
REAP_EVERY = 5.0
|
|
22
|
+
STATUSES = ("starting", "running", "ended", "error", "timed_out")
|
|
23
|
+
|
|
24
|
+
CREATE_SCHEMA = {
|
|
25
|
+
"type": "object", "additionalProperties": False, "required": [],
|
|
26
|
+
"properties": {
|
|
27
|
+
"phone": {"type": "string", "minLength": 1, "maxLength": 128,
|
|
28
|
+
"description": "The iPhone to hold, by its name or id in Mobster, or \"any\" for the first free one "
|
|
29
|
+
"(default: the server's phone)."},
|
|
30
|
+
"timeout_s": {"type": "integer", "minimum": 30, "maximum": MAX_TIMEOUT, "default": DEFAULT_TIMEOUT,
|
|
31
|
+
"description": "The session ends this many seconds after it starts."},
|
|
32
|
+
"idle_s": {"type": "integer", "minimum": 30, "maximum": MAX_IDLE, "default": DEFAULT_IDLE,
|
|
33
|
+
"description": "It ends after this many seconds with no call, unless keep_alive."},
|
|
34
|
+
"keep_alive": {"type": "boolean", "default": False,
|
|
35
|
+
"description": "Keep it until timeout_s even when no call comes."},
|
|
36
|
+
"record": {"type": "boolean", "default": True, "description": "Keep a recording of the screen."},
|
|
37
|
+
"metadata": {"type": "object", "additionalProperties": {"type": "string", "maxLength": 200},
|
|
38
|
+
"description": "Up to 16 strings of yours, kept with the session."},
|
|
39
|
+
}}
|
|
40
|
+
SESSION_SCHEMA = {
|
|
41
|
+
"type": "object", "required": ["id", "status", "phone", "created_at", "expires_at", "live_url", "events"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": {"type": "string"}, "status": {"type": "string", "enum": list(STATUSES)},
|
|
44
|
+
"phone": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}}},
|
|
45
|
+
"created_at": {"type": "string", "format": "date-time"},
|
|
46
|
+
"started_at": {"type": ["string", "null"], "format": "date-time"},
|
|
47
|
+
"ended_at": {"type": ["string", "null"], "format": "date-time"},
|
|
48
|
+
"expires_at": {"type": "string", "format": "date-time"},
|
|
49
|
+
"ended_because": {"type": ["string", "null"]},
|
|
50
|
+
"keep_alive": {"type": "boolean"}, "record": {"type": "boolean"},
|
|
51
|
+
"metadata": {"type": "object", "additionalProperties": {"type": "string"}},
|
|
52
|
+
"live_url": {"type": ["string", "null"], "description": "The viewer page, while the session runs"},
|
|
53
|
+
"events": {"type": "integer", "description": "How many events it has"},
|
|
54
|
+
"frames": {"type": "integer", "description": "How many frames its recording has"}}}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _iso(seconds):
|
|
58
|
+
return datetime.fromtimestamp(seconds, timezone.utc).isoformat(timespec="seconds") if seconds else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Session:
|
|
63
|
+
id: str
|
|
64
|
+
client: str # the token's client name
|
|
65
|
+
phone: dict # {id, name}
|
|
66
|
+
timeout_s: int
|
|
67
|
+
idle_s: int
|
|
68
|
+
keep_alive: bool
|
|
69
|
+
record: bool
|
|
70
|
+
metadata: dict
|
|
71
|
+
folder: Path
|
|
72
|
+
key: str # the viewer key (live view), never in logs
|
|
73
|
+
created_at: float
|
|
74
|
+
status: str = "starting"
|
|
75
|
+
started_at: float = 0.0
|
|
76
|
+
ended_at: float = 0.0
|
|
77
|
+
ended_because: str = None
|
|
78
|
+
last_call: float = 0.0
|
|
79
|
+
device_session: object = None # the ToolSet's direct session
|
|
80
|
+
video: object = None
|
|
81
|
+
frames: int = 0
|
|
82
|
+
events: list = field(default_factory=list)
|
|
83
|
+
changed: threading.Condition = field(default_factory=threading.Condition)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def open(self):
|
|
87
|
+
return self.status in ("starting", "running")
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def expires_at(self):
|
|
91
|
+
return (self.started_at or self.created_at) + self.timeout_s
|
|
92
|
+
|
|
93
|
+
def public(self, base):
|
|
94
|
+
return {"id": self.id, "status": self.status, "phone": dict(self.phone),
|
|
95
|
+
"created_at": _iso(self.created_at), "started_at": _iso(self.started_at),
|
|
96
|
+
"ended_at": _iso(self.ended_at), "expires_at": _iso(self.expires_at),
|
|
97
|
+
"ended_because": self.ended_because, "keep_alive": self.keep_alive, "record": self.record,
|
|
98
|
+
"metadata": dict(self.metadata),
|
|
99
|
+
"live_url": f"{base}/v1/sessions/{self.id}/view?key={self.key}" if self.open else None,
|
|
100
|
+
"events": len(self.events), "frames": self.frames}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Sessions:
|
|
104
|
+
"""Every session of one App API, and the thread that ends the ones past their time."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, toolset, folder, *, clock=time.time, video_factory=None, start_threads=True):
|
|
107
|
+
self.toolset = toolset
|
|
108
|
+
self.folder = Path(folder)
|
|
109
|
+
self.clock = clock
|
|
110
|
+
self.video_factory = video_factory or _wda_video
|
|
111
|
+
self.items = {}
|
|
112
|
+
self.lock = threading.RLock()
|
|
113
|
+
self.closing = threading.Event()
|
|
114
|
+
self._reaper = None
|
|
115
|
+
if start_threads:
|
|
116
|
+
self._reaper = threading.Thread(target=self._reap_loop, name="mobster-api-sessions", daemon=True)
|
|
117
|
+
self._reaper.start()
|
|
118
|
+
|
|
119
|
+
def create(self, inputs, client):
|
|
120
|
+
"""A new session for ``client`` on the phone ``inputs`` names: the Session, or reading.Failure."""
|
|
121
|
+
from ..mcp_server.tools import ToolError
|
|
122
|
+
metadata = inputs.get("metadata") or {}
|
|
123
|
+
if len(metadata) > 16:
|
|
124
|
+
raise reading.Failure(400, "bad_input", "metadata holds up to 16 strings.")
|
|
125
|
+
phone = self._pick(inputs.get("phone"))
|
|
126
|
+
now = self.clock()
|
|
127
|
+
session = Session(secrets.token_hex(ID_HEX // 2), client, {}, inputs.get("timeout_s", DEFAULT_TIMEOUT),
|
|
128
|
+
inputs.get("idle_s", DEFAULT_IDLE), bool(inputs.get("keep_alive", False)),
|
|
129
|
+
bool(inputs.get("record", True)), dict(metadata), Path(), secrets.token_urlsafe(24), now,
|
|
130
|
+
last_call=now)
|
|
131
|
+
with self.lock:
|
|
132
|
+
if self.held(phone) is not None:
|
|
133
|
+
raise reading.Failure(409, "busy", "A session already holds that phone. End it, or pick another "
|
|
134
|
+
"phone.")
|
|
135
|
+
session.folder = self._folder(session.id)
|
|
136
|
+
self.items[session.id] = session
|
|
137
|
+
try:
|
|
138
|
+
device = self.toolset._device_session(phone) if phone else self.toolset._target_session({})
|
|
139
|
+
except ToolError as error:
|
|
140
|
+
self._finish(session, "error", str(error))
|
|
141
|
+
raise reading.Failure(503, "no_phone", str(error)) from None
|
|
142
|
+
record = getattr(device.run, "record", None) or {}
|
|
143
|
+
claimed = {"id": record.get("id") or getattr(device.run, "device_id", "") or "",
|
|
144
|
+
"name": record.get("name") or getattr(device.run, "device_name", "") or ""}
|
|
145
|
+
with self.lock:
|
|
146
|
+
other = next((s for s in self.items.values() if s is not session and s.open
|
|
147
|
+
and s.phone.get("id") == claimed["id"]), None)
|
|
148
|
+
if other is None:
|
|
149
|
+
session.phone, session.device_session = claimed, device
|
|
150
|
+
if other is not None:
|
|
151
|
+
# Refused before it owns the device session, so ending it leaves the other session's alone.
|
|
152
|
+
self._finish(session, "error", "another session holds the phone")
|
|
153
|
+
raise reading.Failure(409, "busy", "A session already holds that phone. End it, or pick another phone.")
|
|
154
|
+
session.status, session.started_at = "running", self.clock()
|
|
155
|
+
self.event(session, "session.started", phone=session.phone, expires_at=_iso(session.expires_at))
|
|
156
|
+
if session.record:
|
|
157
|
+
self._start_recording(session)
|
|
158
|
+
return session
|
|
159
|
+
|
|
160
|
+
def end(self, session, why="the client ended it", status="ended"):
|
|
161
|
+
"""End ``session`` (idempotent): its phone goes back to the Mac app and other clients."""
|
|
162
|
+
if not session.open:
|
|
163
|
+
return session
|
|
164
|
+
self._finish(session, status, why)
|
|
165
|
+
return session
|
|
166
|
+
|
|
167
|
+
def _finish(self, session, status, why):
|
|
168
|
+
with session.changed:
|
|
169
|
+
if not session.open:
|
|
170
|
+
return
|
|
171
|
+
session.status, session.ended_at, session.ended_because = status, self.clock(), why
|
|
172
|
+
video = session.video
|
|
173
|
+
if video is not None:
|
|
174
|
+
try:
|
|
175
|
+
video.close()
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
device = session.device_session
|
|
179
|
+
if device is not None and getattr(device, "mode", "") == "direct":
|
|
180
|
+
try:
|
|
181
|
+
self.toolset._close_direct(device, f"The App API session ended: {why}.", timeout=5)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
self.event(session, "session.ended", status=status, because=why)
|
|
185
|
+
self._save(session)
|
|
186
|
+
|
|
187
|
+
def close(self):
|
|
188
|
+
self.closing.set()
|
|
189
|
+
for session in list(self.items.values()):
|
|
190
|
+
self.end(session, "the App API stopped")
|
|
191
|
+
|
|
192
|
+
def get(self, ident, client):
|
|
193
|
+
"""The session ``ident`` names that ``client`` made: reading.Failure 404 otherwise (another client's session is
|
|
194
|
+
answered as missing)."""
|
|
195
|
+
with self.lock:
|
|
196
|
+
session = self.items.get(str(ident or ""))
|
|
197
|
+
if session is None or session.client != client:
|
|
198
|
+
raise reading.Failure(404, "unknown_session", "No session has that id. GET /v1/sessions lists yours.")
|
|
199
|
+
return session
|
|
200
|
+
|
|
201
|
+
def for_client(self, client):
|
|
202
|
+
with self.lock:
|
|
203
|
+
return sorted((s for s in self.items.values() if s.client == client), key=lambda s: -s.created_at)
|
|
204
|
+
|
|
205
|
+
def held(self, phone):
|
|
206
|
+
"""The open session holding the phone ``phone`` names (its id or name, any case), or None."""
|
|
207
|
+
folded = str(phone or "").strip().casefold()
|
|
208
|
+
if not folded:
|
|
209
|
+
return None
|
|
210
|
+
with self.lock:
|
|
211
|
+
return next((s for s in self.items.values() if s.open and folded in
|
|
212
|
+
{str(s.phone.get("id") or "").casefold(), str(s.phone.get("name") or "").casefold()}), None)
|
|
213
|
+
|
|
214
|
+
def by_key(self, ident, key):
|
|
215
|
+
"""The open session ``ident`` names whose viewer key is ``key``, or None."""
|
|
216
|
+
import hmac
|
|
217
|
+
with self.lock:
|
|
218
|
+
session = self.items.get(str(ident or ""))
|
|
219
|
+
if session is None or not session.open or not key or not hmac.compare_digest(str(key), session.key):
|
|
220
|
+
return None
|
|
221
|
+
return session
|
|
222
|
+
|
|
223
|
+
def video_for(self, session):
|
|
224
|
+
"""The session phone's video relay, started on first use (a session made with record false has none yet); None
|
|
225
|
+
for a device with no stream."""
|
|
226
|
+
with self.lock:
|
|
227
|
+
if session.video is None and session.open:
|
|
228
|
+
record = getattr(getattr(session.device_session, "run", None), "record", None) or {}
|
|
229
|
+
try:
|
|
230
|
+
session.video = self.video_factory(record)
|
|
231
|
+
except Exception:
|
|
232
|
+
session.video = None
|
|
233
|
+
return session.video
|
|
234
|
+
|
|
235
|
+
def touch(self, session):
|
|
236
|
+
session.last_call = self.clock()
|
|
237
|
+
device = session.device_session
|
|
238
|
+
if device is not None and hasattr(device, "touch"):
|
|
239
|
+
device.touch()
|
|
240
|
+
|
|
241
|
+
def event(self, session, kind, **data):
|
|
242
|
+
"""Add one event (``{seq, at, type, ...}``) and wake the session's event streams. No screen text."""
|
|
243
|
+
with session.changed:
|
|
244
|
+
item = {"seq": len(session.events) + 1, "at": _iso(self.clock()), "type": kind, **data}
|
|
245
|
+
if len(session.events) < MAX_EVENTS:
|
|
246
|
+
session.events.append(item)
|
|
247
|
+
session.changed.notify_all()
|
|
248
|
+
return item
|
|
249
|
+
|
|
250
|
+
def stream(self, session, after=0, heartbeat=10.0):
|
|
251
|
+
"""Server-sent events of ``session`` from sequence ``after``, until it ends: lines of bytes."""
|
|
252
|
+
sent = after
|
|
253
|
+
while True:
|
|
254
|
+
with session.changed:
|
|
255
|
+
pending = [e for e in session.events if e["seq"] > sent]
|
|
256
|
+
if not pending and session.open:
|
|
257
|
+
session.changed.wait(heartbeat)
|
|
258
|
+
pending = [e for e in session.events if e["seq"] > sent]
|
|
259
|
+
done = not session.open
|
|
260
|
+
if not pending:
|
|
261
|
+
if done:
|
|
262
|
+
return
|
|
263
|
+
yield b": keepalive\n\n"
|
|
264
|
+
continue
|
|
265
|
+
for item in pending:
|
|
266
|
+
sent = item["seq"]
|
|
267
|
+
yield f"id: {item['seq']}\nevent: {item['type']}\ndata: {json.dumps(item)}\n\n".encode()
|
|
268
|
+
if done and all(e["seq"] <= sent for e in session.events):
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
def _start_recording(self, session):
|
|
272
|
+
if self.video_for(session) is None:
|
|
273
|
+
self.event(session, "recording.off", why="this phone has no video stream")
|
|
274
|
+
return
|
|
275
|
+
(session.folder / "frames").mkdir(mode=0o700, exist_ok=True)
|
|
276
|
+
threading.Thread(target=self._record, args=(session,), name="mobster-api-recorder", daemon=True).start()
|
|
277
|
+
|
|
278
|
+
def _record(self, session):
|
|
279
|
+
video = session.video
|
|
280
|
+
try:
|
|
281
|
+
viewer = video.subscribe()
|
|
282
|
+
except Exception:
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
for frame in video.paced_frames(viewer, lambda: RECORD_FPS):
|
|
286
|
+
if not session.open or session.frames >= MAX_FRAMES:
|
|
287
|
+
break
|
|
288
|
+
session.frames += 1
|
|
289
|
+
at_ms = int((self.clock() - session.started_at) * 1000)
|
|
290
|
+
path = session.folder / "frames" / f"{session.frames:05d}-{at_ms:09d}.jpg"
|
|
291
|
+
handle = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
292
|
+
with os.fdopen(handle, "wb") as out:
|
|
293
|
+
out.write(frame[2])
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
finally:
|
|
297
|
+
try:
|
|
298
|
+
video.unsubscribe(viewer)
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
def recording(self, session):
|
|
303
|
+
"""The session's recording as MP4 bytes. reading.Failure when it has no frames or ffmpeg is missing."""
|
|
304
|
+
import io
|
|
305
|
+
|
|
306
|
+
from PIL import Image
|
|
307
|
+
|
|
308
|
+
from .. import run_gif
|
|
309
|
+
frames = sorted((session.folder / "frames").glob("*.jpg")) if (session.folder / "frames").is_dir() else []
|
|
310
|
+
if not frames:
|
|
311
|
+
raise reading.Failure(404, "no_recording", "This session has no recording: it was made with record "
|
|
312
|
+
"false, or its phone has no video stream.")
|
|
313
|
+
ffmpeg = run_gif.ffmpeg_path()
|
|
314
|
+
if ffmpeg is None:
|
|
315
|
+
raise reading.Failure(503, "no_ffmpeg", "Making the MP4 needs ffmpeg. Install it with brew install "
|
|
316
|
+
"ffmpeg, then ask again.")
|
|
317
|
+
stamps = [int(p.stem.split("-")[1]) for p in frames]
|
|
318
|
+
end_ms = int(((session.ended_at or self.clock()) - session.started_at) * 1000)
|
|
319
|
+
images = []
|
|
320
|
+
for index, path in enumerate(frames):
|
|
321
|
+
following = stamps[index + 1] if index + 1 < len(frames) else max(end_ms, stamps[index] + 500)
|
|
322
|
+
image = Image.open(io.BytesIO(path.read_bytes())).convert("RGB")
|
|
323
|
+
if image.width % 2 or image.height % 2:
|
|
324
|
+
image = image.crop((0, 0, image.width - image.width % 2, image.height - image.height % 2))
|
|
325
|
+
images.append((image, max(1, following - stamps[index])))
|
|
326
|
+
data = run_gif.encode_mp4(images, ffmpeg)
|
|
327
|
+
if not data:
|
|
328
|
+
raise reading.Failure(502, "encode_failed", "ffmpeg couldn't encode the recording. Try again.")
|
|
329
|
+
return data
|
|
330
|
+
|
|
331
|
+
def logs(self, session):
|
|
332
|
+
"""The session's events and its device run's action journal (operations and outcomes, no arguments)."""
|
|
333
|
+
actions = []
|
|
334
|
+
run_dir = getattr(getattr(session.device_session, "run", None), "run_dir", None)
|
|
335
|
+
path = Path(run_dir) / "actions.jsonl" if run_dir else None
|
|
336
|
+
if path is not None and path.is_file():
|
|
337
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines()[-2000:]:
|
|
338
|
+
try:
|
|
339
|
+
actions.append(json.loads(line))
|
|
340
|
+
except ValueError:
|
|
341
|
+
continue
|
|
342
|
+
return {"session": session.id, "events": list(session.events), "actions": actions}
|
|
343
|
+
|
|
344
|
+
def _pick(self, phone):
|
|
345
|
+
"""The phone to hold: the one named, or for "any" the first ready device no session holds."""
|
|
346
|
+
if str(phone or "").strip().casefold() != "any":
|
|
347
|
+
return phone
|
|
348
|
+
records = self.toolset._device_records(fresh=True)
|
|
349
|
+
for record in records:
|
|
350
|
+
if record.get("state") != "ready" or not record.get("wdaUrl") or not self.toolset._allowed(record):
|
|
351
|
+
continue
|
|
352
|
+
if self.held(record["id"]) is None:
|
|
353
|
+
return record["id"]
|
|
354
|
+
raise reading.Failure(503, "no_free_phone", "Every phone is held or isn't ready. End a session, or try "
|
|
355
|
+
"again when a phone is free.")
|
|
356
|
+
|
|
357
|
+
def _folder(self, ident):
|
|
358
|
+
self.folder.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
359
|
+
kept = sorted((p for p in self.folder.iterdir() if p.is_dir()), key=lambda p: p.stat().st_mtime)
|
|
360
|
+
for old in kept[:max(0, len(kept) - KEPT_SESSIONS + 1)]:
|
|
361
|
+
if old.name not in self.items:
|
|
362
|
+
import shutil
|
|
363
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
364
|
+
folder = self.folder / ident
|
|
365
|
+
folder.mkdir(mode=0o700)
|
|
366
|
+
return folder
|
|
367
|
+
|
|
368
|
+
def _save(self, session):
|
|
369
|
+
try:
|
|
370
|
+
data = {**session.public(""), "live_url": None, "client": session.client, "events": session.events}
|
|
371
|
+
(session.folder / "session.json").write_text(json.dumps(data, indent=1))
|
|
372
|
+
os.chmod(session.folder / "session.json", 0o600)
|
|
373
|
+
except OSError:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
def reap(self):
|
|
377
|
+
now = self.clock()
|
|
378
|
+
for session in list(self.items.values()):
|
|
379
|
+
if not session.open:
|
|
380
|
+
continue
|
|
381
|
+
if now >= session.expires_at:
|
|
382
|
+
self.end(session, f"it reached its {session.timeout_s}-second timeout", "timed_out")
|
|
383
|
+
elif not session.keep_alive and now - session.last_call >= session.idle_s:
|
|
384
|
+
self.end(session, f"no call came for {session.idle_s} seconds", "timed_out")
|
|
385
|
+
else:
|
|
386
|
+
device = session.device_session
|
|
387
|
+
if device is not None and hasattr(device, "touch"):
|
|
388
|
+
device.touch() # the ToolSet releases a direct device idle 90 s: the session keeps it
|
|
389
|
+
|
|
390
|
+
def _reap_loop(self):
|
|
391
|
+
while not self.closing.wait(REAP_EVERY):
|
|
392
|
+
try:
|
|
393
|
+
self.reap()
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _wda_video(record):
|
|
399
|
+
"""The phone's MJPEG relay (wda.video.WdaVideo), or None for a device with no stream."""
|
|
400
|
+
from ..wda.video import WdaVideo
|
|
401
|
+
url = record.get("wdaUrl")
|
|
402
|
+
if not url:
|
|
403
|
+
return None
|
|
404
|
+
return WdaVideo(url, mjpeg_url=record.get("mjpegUrl"), screenshot_fallback=False)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""The App API's tokens: one per client you let call it, created and revoked by you (``mobster api tokens``)."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import fcntl
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import secrets
|
|
11
|
+
import stat
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
FILE = "app-api-tokens.json"
|
|
17
|
+
LOCK = "app-api-tokens.lock"
|
|
18
|
+
PREFIX = "mob_"
|
|
19
|
+
TOKEN = re.compile(r"mob_[A-Za-z0-9_-]{43}")
|
|
20
|
+
ID = re.compile(r"[a-f0-9]{12}")
|
|
21
|
+
NAME = re.compile(r"[^\x00-\x1f\x7f]{1,40}")
|
|
22
|
+
MAX_TOKENS = 50
|
|
23
|
+
# A token's last use is written at most this often (a read per request would rewrite the file each time).
|
|
24
|
+
USE_EVERY = 60
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TokenError(ValueError):
|
|
28
|
+
"""A name or id the store can't take, or a store it can't trust: one plain sentence."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _digest(token):
|
|
32
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def public(record):
|
|
36
|
+
"""A token as `mobster api tokens list` shows it: never its hash."""
|
|
37
|
+
return {key: record.get(key) for key in ("id", "name", "start", "createdAt", "lastUsedAt")}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TokenStore:
|
|
41
|
+
def __init__(self, data_dir, clock=time.time):
|
|
42
|
+
self.data_dir = Path(data_dir)
|
|
43
|
+
self.path = self.data_dir / FILE
|
|
44
|
+
self.lock_path = self.data_dir / LOCK
|
|
45
|
+
self.clock = clock
|
|
46
|
+
self._used = {}
|
|
47
|
+
|
|
48
|
+
def trusted(self):
|
|
49
|
+
"""Whether the file is one only this user could have written (a missing file is: it holds no token). An
|
|
50
|
+
untrusted file refuses every token, so a file someone else could edit never lets a request in."""
|
|
51
|
+
try:
|
|
52
|
+
info = self.path.lstat()
|
|
53
|
+
except FileNotFoundError:
|
|
54
|
+
return True
|
|
55
|
+
except OSError:
|
|
56
|
+
return False
|
|
57
|
+
return stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid() and not info.st_mode & 0o022
|
|
58
|
+
|
|
59
|
+
def _read(self):
|
|
60
|
+
if not self.trusted():
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
return []
|
|
66
|
+
except (OSError, ValueError, UnicodeDecodeError):
|
|
67
|
+
return None
|
|
68
|
+
items = data.get("tokens") if isinstance(data, dict) else None
|
|
69
|
+
if not isinstance(items, list):
|
|
70
|
+
return None
|
|
71
|
+
return [t for t in items if isinstance(t, dict) and isinstance(t.get("id"), str) and ID.fullmatch(t["id"])
|
|
72
|
+
and isinstance(t.get("hash"), str) and len(t["hash"]) == 64 and isinstance(t.get("name"), str)]
|
|
73
|
+
|
|
74
|
+
def all(self):
|
|
75
|
+
"""Every token, as ``public`` shows it. TokenError when the file can't be trusted or read."""
|
|
76
|
+
tokens = self._read()
|
|
77
|
+
if tokens is None:
|
|
78
|
+
raise TokenError(f"Mobster can't trust {self.path}: only you may write it. Remove it, then make your "
|
|
79
|
+
"tokens again with mobster api tokens create.")
|
|
80
|
+
return [public(t) for t in tokens]
|
|
81
|
+
|
|
82
|
+
@contextlib.contextmanager
|
|
83
|
+
def _edit(self):
|
|
84
|
+
self.data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
85
|
+
fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
86
|
+
try:
|
|
87
|
+
# Read, change and write under one lock, so two Mobster processes never lose each other's changes.
|
|
88
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
89
|
+
tokens = self._read()
|
|
90
|
+
if tokens is None:
|
|
91
|
+
raise TokenError(f"Mobster can't trust {self.path}: only you may write it. Remove it, then make "
|
|
92
|
+
"your tokens again with mobster api tokens create.")
|
|
93
|
+
yield tokens
|
|
94
|
+
handle, temporary = tempfile.mkstemp(dir=self.data_dir, prefix=".app-api-tokens.")
|
|
95
|
+
try:
|
|
96
|
+
with os.fdopen(handle, "w", encoding="utf-8") as out:
|
|
97
|
+
os.fchmod(out.fileno(), 0o600)
|
|
98
|
+
json.dump({"version": 1, "tokens": tokens}, out, indent=1)
|
|
99
|
+
os.replace(temporary, self.path)
|
|
100
|
+
except BaseException:
|
|
101
|
+
with contextlib.suppress(OSError):
|
|
102
|
+
os.unlink(temporary)
|
|
103
|
+
raise
|
|
104
|
+
finally:
|
|
105
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
106
|
+
os.close(fd)
|
|
107
|
+
|
|
108
|
+
def create(self, name):
|
|
109
|
+
"""A new token for the client ``name``: (its public record, the token). The token is never kept."""
|
|
110
|
+
name = " ".join(str(name or "").split())
|
|
111
|
+
if not NAME.fullmatch(name):
|
|
112
|
+
raise TokenError("A token's name is 1 to 40 characters, such as \"n8n\" or \"my script\".")
|
|
113
|
+
token = PREFIX + secrets.token_urlsafe(32)
|
|
114
|
+
with self._edit() as tokens:
|
|
115
|
+
if any(t["name"].casefold() == name.casefold() for t in tokens):
|
|
116
|
+
raise TokenError(f"A token is already named “{name}”. Revoke it first, or pick another name.")
|
|
117
|
+
if len(tokens) >= MAX_TOKENS:
|
|
118
|
+
raise TokenError(f"You have {MAX_TOKENS} tokens. Revoke one you no longer use first.")
|
|
119
|
+
ids = {t["id"] for t in tokens}
|
|
120
|
+
ident = secrets.token_hex(6)
|
|
121
|
+
while ident in ids:
|
|
122
|
+
ident = secrets.token_hex(6)
|
|
123
|
+
record = {"id": ident, "name": name, "start": token[:8], "hash": _digest(token),
|
|
124
|
+
"createdAt": int(self.clock()), "lastUsedAt": None}
|
|
125
|
+
tokens.append(record)
|
|
126
|
+
return public(record), token
|
|
127
|
+
|
|
128
|
+
def revoke(self, which):
|
|
129
|
+
"""Remove the token ``which`` names (its id or its name): its public record. TokenError when none does."""
|
|
130
|
+
which = " ".join(str(which or "").split())
|
|
131
|
+
with self._edit() as tokens:
|
|
132
|
+
found = [t for t in tokens if t["id"] == which or t["name"].casefold() == which.casefold()]
|
|
133
|
+
if not found:
|
|
134
|
+
raise TokenError(f"No token is named “{which[:40]}”. mobster api tokens list shows them.")
|
|
135
|
+
tokens[:] = [t for t in tokens if t is not found[0]]
|
|
136
|
+
self._used.pop(found[0]["id"], None)
|
|
137
|
+
return public(found[0])
|
|
138
|
+
|
|
139
|
+
def verify(self, token):
|
|
140
|
+
"""The public record of the token ``token`` is, or None: a malformed token, an unknown or revoked one, or a file
|
|
141
|
+
Mobster can't trust."""
|
|
142
|
+
if not isinstance(token, str) or not TOKEN.fullmatch(token):
|
|
143
|
+
return None
|
|
144
|
+
tokens = self._read()
|
|
145
|
+
if not tokens:
|
|
146
|
+
return None
|
|
147
|
+
digest = _digest(token).encode()
|
|
148
|
+
found = None
|
|
149
|
+
for record in tokens:
|
|
150
|
+
if hmac.compare_digest(digest, record["hash"].encode()):
|
|
151
|
+
found = record
|
|
152
|
+
if found is not None:
|
|
153
|
+
self._note_use(found["id"])
|
|
154
|
+
return public(found) if found else None
|
|
155
|
+
|
|
156
|
+
def _note_use(self, ident):
|
|
157
|
+
now = self.clock()
|
|
158
|
+
if now - self._used.get(ident, 0) < USE_EVERY:
|
|
159
|
+
return
|
|
160
|
+
self._used[ident] = now
|
|
161
|
+
try:
|
|
162
|
+
with self._edit() as tokens:
|
|
163
|
+
for record in tokens:
|
|
164
|
+
if record["id"] == ident:
|
|
165
|
+
record["lastUsedAt"] = int(now)
|
|
166
|
+
except (OSError, TokenError):
|
|
167
|
+
pass # the last use is a note: a full disk never refuses a request
|