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,892 @@
|
|
|
1
|
+
"""The iPhone tools an outside agent gets over MCP while it runs a Mobster task (ai_plans)."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
from .. import offers
|
|
12
|
+
from ..mcp_server.protocol import ACTS, READ_ONLY, ToolResult
|
|
13
|
+
from ..wda.point_gestures import DRAG_SCHEMA, FLICK_SCHEMA, GESTURE_OPS, HOLD_SCHEMA, validate_gesture_request
|
|
14
|
+
|
|
15
|
+
NAMES = ("screen", "act", "tap", "tap_point", "drag_point", "hold_point", "flick_point", "type_text", "swipe",
|
|
16
|
+
"long_press", "open_app", "home", "read_list", "scroll_to",
|
|
17
|
+
"dismiss", "open_url", "ask_user", "wait")
|
|
18
|
+
CHAINABLE = ("tap", "type_text", "swipe", "long_press", "open_app", "home", "scroll_to", "dismiss", "wait")
|
|
19
|
+
MAX_CHAIN = 8
|
|
20
|
+
COMMIT_OPS = frozenset({"TAP", "LONG_PRESS", "SUBMIT", "TYPE_SUBMIT"})
|
|
21
|
+
SWIPES = {"up": "SWIPE_UP", "down": "SWIPE_DOWN", "left": "SWIPE_LEFT", "right": "SWIPE_RIGHT"}
|
|
22
|
+
ELEMENT_ID = re.compile(r"e[0-9]{1,4}")
|
|
23
|
+
WAIT_MAX = 10.0
|
|
24
|
+
QUESTION_CHARS, CHOICE_CHARS = 300, 120
|
|
25
|
+
LIST_ROWS_SHOWN = 80
|
|
26
|
+
# Facts a screen can show word for word: amount, version, time, percentage, code, number.
|
|
27
|
+
FACT = re.compile(r"\$\s?\d[\d,]*(?:\.\d+)?|\b\d+(?:[.:]\d+)+\b|\b\d+(?:\.\d+)?%|\b[A-Z]{2,}-?\d{3,}\b|\b\d[\d,]*\b")
|
|
28
|
+
PROOF_ITEMS = 12
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def step_name(step):
|
|
32
|
+
"""A chain step as the agent wrote it: the tool and what it names. Never the text a step types."""
|
|
33
|
+
tool = step.get("tool") if isinstance(step, dict) else None
|
|
34
|
+
if not isinstance(tool, str) or not tool:
|
|
35
|
+
return "a step with no tool" # an unrun step is never checked, so it can be anything the agent sent
|
|
36
|
+
keys = ("label", "element", "app", "direction") + (("text",) if tool == "scroll_to" else ())
|
|
37
|
+
target = next((step[k] for k in keys if isinstance(step.get(k), str) and step[k]), None)
|
|
38
|
+
return f"{tool} {json.dumps(target)}" if target else str(tool)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ToolProblem(Exception):
|
|
42
|
+
"""A tool error: one plain sentence telling the agent what to do next (the result's isError)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RunStopped(ToolProblem):
|
|
46
|
+
"""The person stopped the task, or the phone guard ended it: no more actions."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def frame_bytes(driver):
|
|
50
|
+
"""The screen as JPEG bytes for the step list."""
|
|
51
|
+
from ..frontier.screen import video_frame
|
|
52
|
+
data = video_frame(driver)
|
|
53
|
+
if data is None:
|
|
54
|
+
capture = getattr(driver, "capture_preview", None)
|
|
55
|
+
if callable(capture) and not getattr(driver, "ax_only_steps", 0):
|
|
56
|
+
try:
|
|
57
|
+
data = capture(timeout=5)
|
|
58
|
+
except Exception:
|
|
59
|
+
data = None
|
|
60
|
+
if isinstance(data, str):
|
|
61
|
+
try:
|
|
62
|
+
data = base64.b64decode(data.split(",", 1)[1] if data.startswith("data:") else data)
|
|
63
|
+
except (ValueError, TypeError):
|
|
64
|
+
return None
|
|
65
|
+
return bytes(data) if isinstance(data, (bytes, bytearray)) and data else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def has_text(element):
|
|
69
|
+
"""Whether a field holds text of its own (not only its placeholder)."""
|
|
70
|
+
value = " ".join(str(element.value or "").split())
|
|
71
|
+
return bool(value) and value not in (" ".join(str(element.placeholder or "").split()),
|
|
72
|
+
" ".join(str(element.label or "").split()))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PhoneTools:
|
|
76
|
+
"""The tools of one run."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, driver, *, apps, goal, emit, approve=None, ask=None, cancelled=None, guard=None,
|
|
79
|
+
build=None, remembered=None, permissions):
|
|
80
|
+
from .. import engines
|
|
81
|
+
self.driver, self.apps, self.goal = driver, dict(apps or {}), goal or ""
|
|
82
|
+
self.out = emit or (lambda event: None)
|
|
83
|
+
self.approve, self.ask = approve, ask
|
|
84
|
+
self.permissions = permissions
|
|
85
|
+
# the person's "Don't ask again" list (permissions.exceptions.Exceptions.contains), or None
|
|
86
|
+
self.remembered = remembered
|
|
87
|
+
self.cancelled = cancelled or (lambda: False)
|
|
88
|
+
build = build or engines.build_frontier
|
|
89
|
+
kwargs = {"guard": guard} if guard is not None else {}
|
|
90
|
+
# No model: only the frontier's reads, actuation, macros, masking and guard are used.
|
|
91
|
+
self.agent = build(driver, None, apps=self.apps, emit=self._trace, cancelled=self.cancelled, **kwargs)
|
|
92
|
+
self.lock = threading.RLock()
|
|
93
|
+
self.snapshot, self.aliases = None, {}
|
|
94
|
+
self.point_image = None # the screenshot the agent last saw: a point it names belongs to this picture
|
|
95
|
+
self.steps = 0
|
|
96
|
+
self.actions = 0
|
|
97
|
+
self.stopped = None # the GuardVerdict that ended the run, once one did
|
|
98
|
+
self.typed = None # (app, text) last typed into a composer: what a Send tap sends, for its card
|
|
99
|
+
# Every screen shown to the agent, as the contract records them,
|
|
100
|
+
# and how many actions may have changed something.
|
|
101
|
+
from ..contract import Contract
|
|
102
|
+
self.ledger = Contract([], self.goal)
|
|
103
|
+
self.changes = 0
|
|
104
|
+
self.approved_send = None # an approved Return that may not have sent: its Send tap isn't asked again
|
|
105
|
+
self.tuned = False
|
|
106
|
+
|
|
107
|
+
def proof(self, answer):
|
|
108
|
+
"""The result's proof (smart_events.final_proof's shape) for a run that changed nothing."""
|
|
109
|
+
from ..contract_screen import plain, quoted
|
|
110
|
+
if self.changes or not answer:
|
|
111
|
+
return []
|
|
112
|
+
facts = list(dict.fromkeys([plain(fact) for fact in FACT.findall(answer)] +
|
|
113
|
+
[plain(name) for name in quoted(answer)]))
|
|
114
|
+
if not facts:
|
|
115
|
+
return []
|
|
116
|
+
proof, rows_used = [], set()
|
|
117
|
+
for fact in facts:
|
|
118
|
+
pattern = re.compile(r"(?<![\w.:$])" + re.escape(fact) + r"(?![\w]|[.:]\d)")
|
|
119
|
+
found = next(((step, app, title, row, original)
|
|
120
|
+
for step, app, title, originals in reversed(self.ledger.shown)
|
|
121
|
+
for row, original in originals.items() if pattern.search(row)), None)
|
|
122
|
+
if found is None:
|
|
123
|
+
return []
|
|
124
|
+
step, app, title, row, original = found
|
|
125
|
+
if (step, row) not in rows_used:
|
|
126
|
+
rows_used.add((step, row))
|
|
127
|
+
proof.append({"quote": original, "app": app, "screen": title or app, "step": step})
|
|
128
|
+
return proof[:PROOF_ITEMS]
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def definitions():
|
|
132
|
+
return [{"name": name, "description": DESCRIPTIONS[name], "inputSchema": SCHEMAS[name],
|
|
133
|
+
"annotations": ANNOTATIONS[name]} for name in NAMES]
|
|
134
|
+
|
|
135
|
+
def call(self, name, arguments):
|
|
136
|
+
"""Run tool ``name``. Returns a ToolResult; a problem is one too, with is_error set."""
|
|
137
|
+
if name == "act":
|
|
138
|
+
return self._act_call(arguments if isinstance(arguments, dict) else {})
|
|
139
|
+
handler = getattr(self, f"tool_{name}", None) if name in NAMES else None
|
|
140
|
+
if handler is None:
|
|
141
|
+
return ToolResult(f"Mobster has no tool named {str(name)[:60]}.", is_error=True)
|
|
142
|
+
arguments = arguments if isinstance(arguments, dict) else {}
|
|
143
|
+
from ..frontier import GuardStop
|
|
144
|
+
try:
|
|
145
|
+
with self.lock:
|
|
146
|
+
self._check_running()
|
|
147
|
+
return handler(arguments)
|
|
148
|
+
except GuardStop as error: # a BaseException: past the handler below
|
|
149
|
+
self.stopped = error.verdict
|
|
150
|
+
return ToolResult(error.verdict.message or "The phone stopped the task.", is_error=True)
|
|
151
|
+
except ToolProblem as problem:
|
|
152
|
+
return ToolResult(str(problem), is_error=True)
|
|
153
|
+
except Exception as error: # noqa: BLE001 -- a tool never takes the run down
|
|
154
|
+
return ToolResult(f"That didn't work ({type(error).__name__}: {str(error)[:160]}). Call screen to "
|
|
155
|
+
"see the phone now.", is_error=True)
|
|
156
|
+
|
|
157
|
+
def _check_running(self):
|
|
158
|
+
if self.stopped is not None:
|
|
159
|
+
raise RunStopped(self.stopped.message or "The task has ended.")
|
|
160
|
+
if self.cancelled():
|
|
161
|
+
raise RunStopped("The person stopped this task. Stop now and say nothing more.")
|
|
162
|
+
|
|
163
|
+
def _act_call(self, arguments):
|
|
164
|
+
"""act through call's rules: a step that fails ends the chain with what was done before it."""
|
|
165
|
+
done = []
|
|
166
|
+
steps = arguments.get("steps") if isinstance(arguments.get("steps"), list) else []
|
|
167
|
+
if not steps:
|
|
168
|
+
return ToolResult("steps is a list of actions, such as [{\"tool\": \"tap\", \"label\": \"Alex\"}].",
|
|
169
|
+
is_error=True)
|
|
170
|
+
last, unrun = None, []
|
|
171
|
+
chain = steps[:MAX_CHAIN]
|
|
172
|
+
for index, step in enumerate(chain):
|
|
173
|
+
name = step.get("tool") if isinstance(step, dict) else None
|
|
174
|
+
if name not in CHAINABLE:
|
|
175
|
+
last = ToolResult(f"Step {index + 1}: tool is one of {', '.join(CHAINABLE)}.", is_error=True)
|
|
176
|
+
break
|
|
177
|
+
last = self.call(name, {k: v for k, v in step.items() if k != "tool" and v is not None})
|
|
178
|
+
if last.is_error:
|
|
179
|
+
break
|
|
180
|
+
done.append(last.text.split("\n", 1)[0])
|
|
181
|
+
if last.no_effect:
|
|
182
|
+
unrun = chain[index + 1:]
|
|
183
|
+
break
|
|
184
|
+
if last is None:
|
|
185
|
+
return ToolResult("Nothing to do.", is_error=True)
|
|
186
|
+
if last.is_error:
|
|
187
|
+
before = ("Done first: " + " ".join(done) + "\n") if done else ""
|
|
188
|
+
failed = len(done) + 1
|
|
189
|
+
return ToolResult(f"{before}Step {failed} didn't work: {last.text}", last.structured, last.images,
|
|
190
|
+
is_error=True)
|
|
191
|
+
rest = last.text.split("\n\n", 1)[1] if "\n\n" in last.text else last.text
|
|
192
|
+
stopped = (f" Step {len(done)} showed no effect ({last.no_effect}), so these didn't run: "
|
|
193
|
+
+ ", ".join(step_name(step) for step in unrun) + ". Look at the screen before you send them again."
|
|
194
|
+
if unrun else "")
|
|
195
|
+
return ToolResult("Done: " + " ".join(done) + stopped + "\n\n" + rest, last.structured, last.images,
|
|
196
|
+
no_effect=last.no_effect)
|
|
197
|
+
|
|
198
|
+
def _trace(self, event):
|
|
199
|
+
"""The frontier's own trace events: the phone guard's stops are kept; the rest is noise here."""
|
|
200
|
+
if isinstance(event, dict) and event.get("event") == "phone_guard":
|
|
201
|
+
self.out(event)
|
|
202
|
+
|
|
203
|
+
def _read(self):
|
|
204
|
+
if not self.tuned:
|
|
205
|
+
self.tuned = True
|
|
206
|
+
self.agent._tune()
|
|
207
|
+
self.agent._guard_check("resume")
|
|
208
|
+
snapshot = self.agent._observe()
|
|
209
|
+
self.agent._check_app(snapshot)
|
|
210
|
+
return snapshot
|
|
211
|
+
|
|
212
|
+
def _show(self, snapshot, image=False, head=""):
|
|
213
|
+
"""The screen as text the agent reads (ids e1 ...), remembered for the next action."""
|
|
214
|
+
from ..frontier import screen_rows
|
|
215
|
+
masked = self.agent._masked(snapshot)
|
|
216
|
+
self.snapshot = snapshot
|
|
217
|
+
self.aliases, lines = screen_rows(masked, self.agent.run_state.receipts)
|
|
218
|
+
app = self.agent._app_name(snapshot) or "Home Screen"
|
|
219
|
+
if snapshot.bundle_id == "com.apple.springboard":
|
|
220
|
+
app = "Home Screen"
|
|
221
|
+
self.ledger.observe(masked, app, self.steps)
|
|
222
|
+
text = (head + "\n\n" if head else "") + f"Screen: {app}\n" + ("\n".join(lines) or "(nothing readable)")
|
|
223
|
+
images = []
|
|
224
|
+
self.point_image = None
|
|
225
|
+
if image:
|
|
226
|
+
part = self.agent._screenshot(snapshot)
|
|
227
|
+
url = ((part or {}).get("image_url") or {}).get("url") or ""
|
|
228
|
+
if url.startswith("data:image/jpeg;base64,"):
|
|
229
|
+
images.append(url.split(",", 1)[1])
|
|
230
|
+
self.point_image = part
|
|
231
|
+
structured = {"app": app, "bundle_id": snapshot.bundle_id or None, "elements": len(self.aliases)}
|
|
232
|
+
return ToolResult(text, structured, images)
|
|
233
|
+
|
|
234
|
+
def _element(self, arguments, required=True):
|
|
235
|
+
"""The element ``element`` (an id from the latest screen) or ``label`` names."""
|
|
236
|
+
from ..frontier import FrontierAgent
|
|
237
|
+
ref, label = arguments.get("element"), arguments.get("label")
|
|
238
|
+
snapshot = self.snapshot
|
|
239
|
+
if snapshot is None or not self._still(snapshot):
|
|
240
|
+
snapshot = self._read()
|
|
241
|
+
fresh = True
|
|
242
|
+
else:
|
|
243
|
+
fresh = False
|
|
244
|
+
if not ref and not label:
|
|
245
|
+
if required:
|
|
246
|
+
raise ToolProblem("Say which element: its id from the latest screen (element: \"e12\") or its "
|
|
247
|
+
"visible label (label).")
|
|
248
|
+
return None, snapshot
|
|
249
|
+
known = self.aliases.get(ref) if ref else None
|
|
250
|
+
if ref and known is None and not label:
|
|
251
|
+
raise ToolProblem(f"{ref} isn't on the latest screen. Call screen for current ids.")
|
|
252
|
+
if known is not None:
|
|
253
|
+
found = known if not fresh else FrontierAgent._find(snapshot, known.label, known.role, known.locator)
|
|
254
|
+
else:
|
|
255
|
+
found = FrontierAgent._find(snapshot, label)
|
|
256
|
+
if found is None:
|
|
257
|
+
what = f"{ref} ({json.dumps(known.label)})" if known is not None else json.dumps(label)
|
|
258
|
+
self._show(snapshot)
|
|
259
|
+
raise ToolProblem(f"{what} isn't on the screen now. Call screen to see what is.")
|
|
260
|
+
return found, snapshot
|
|
261
|
+
|
|
262
|
+
def _still(self, snapshot):
|
|
263
|
+
"""Whether ``snapshot`` is still the screen: nothing moved since it was read (one fast check)."""
|
|
264
|
+
from ..wda.guards import unchanged_since
|
|
265
|
+
try:
|
|
266
|
+
return unchanged_since(getattr(self.driver, "frame_clock", None), getattr(snapshot, "read_started", 0.0))
|
|
267
|
+
except Exception:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
def _approval(self, op, element, snapshot, text, declared):
|
|
271
|
+
"""Ask the person before ``op`` on ``element`` when it commits something, raising ToolProblem unless approved;
|
|
272
|
+
returns the target to act on, read again after a card, where a change from what the card showed refuses, its
|
|
273
|
+
screen, and whether the gate read it as a commit or asked."""
|
|
274
|
+
from .. import task_policy as policy
|
|
275
|
+
gate = self.permissions
|
|
276
|
+
subject = policy.approval_subject(snapshot.elements, element) if op in ("TAP", "LONG_PRESS") else element
|
|
277
|
+
label, role = subject.label or "", subject.role
|
|
278
|
+
# A hold on a control is read as a tap on it (Smart asks for both alike): holding Send can send.
|
|
279
|
+
reading = "TAP" if op == "LONG_PRESS" else op
|
|
280
|
+
commits = False
|
|
281
|
+
if op in COMMIT_OPS:
|
|
282
|
+
tier = policy.action_risk_tier(reading, label, self.goal)
|
|
283
|
+
kind = policy.approval_kind(reading, label, risk_tier=getattr(tier, "value", tier), role=role)
|
|
284
|
+
from ..permissions.consequence import FOLDERS, clears_query_field, keypad_key
|
|
285
|
+
from ..permissions.rules import norm
|
|
286
|
+
|
|
287
|
+
# A calculator's Clear key and a mailbox row named Trash are no act.
|
|
288
|
+
from ..state import pixel_read
|
|
289
|
+
pixels = pixel_read(snapshot)
|
|
290
|
+
folder = (role == "Cell" or pixels) and norm(label) in FOLDERS and subject.rect[2] >= .6
|
|
291
|
+
# A search bar's Clear empties a query, not a draft or a note: no act.
|
|
292
|
+
query = clears_query_field(label, role, subject, snapshot.elements,
|
|
293
|
+
getattr(snapshot, "bundle_id", "") or "", pixels)
|
|
294
|
+
commits = (kind == "commit" and not folder and not query
|
|
295
|
+
and not keypad_key(label, role, snapshot.elements, pixels)) or declared
|
|
296
|
+
if commits or op not in ("TAP", "LONG_PRESS"):
|
|
297
|
+
self.changes += 1 # a commit, or typing: the result needs the screen to show it, which proof doesn't check
|
|
298
|
+
act = policy.commit_act(label, self.goal, role, reading) if commits else None
|
|
299
|
+
if op in ("TYPE_SUBMIT", "SUBMIT") and act == "submit" and self._field(element) == "composer":
|
|
300
|
+
act = "send" # Return in a message composer sends it, whatever the field is called ("iMessage")
|
|
301
|
+
app_name = self.agent._app_name(snapshot) or None
|
|
302
|
+
from ..permissions import action_from
|
|
303
|
+
typed = text if op in ("TYPE", "SET_TEXT", "TYPE_SUBMIT") else None
|
|
304
|
+
decision = gate.decide(action_from(op, subject, self.agent._masked(snapshot), app_name=app_name or "",
|
|
305
|
+
text=typed), existing_act=act)
|
|
306
|
+
if decision.decision == "allow":
|
|
307
|
+
return element, snapshot, commits
|
|
308
|
+
# It asks: with Ask before acting off too, since a one-time code asks in every mode.
|
|
309
|
+
asker, reason, title = self.approve or self.ask, decision.reason or None, None
|
|
310
|
+
if act is None:
|
|
311
|
+
title, act = decision.title, decision.act or "other"
|
|
312
|
+
remember = gate.remember(decision, getattr(snapshot, "bundle_id", "") or "", app_name or "")
|
|
313
|
+
if asker is None:
|
|
314
|
+
raise ToolProblem(f"{json.dumps(label or 'That')} needs the person's OK, and nobody is watching this "
|
|
315
|
+
"task to give it, so Mobster didn't do it. Finish and say what you need.")
|
|
316
|
+
# The card shows the message as it will go and who it goes to.
|
|
317
|
+
payload = text if op in ("TYPE_SUBMIT", "SUBMIT") and text else None
|
|
318
|
+
if payload is None and act in policy.MESSAGE_ACTS and self.typed and self.typed[0] == app_name:
|
|
319
|
+
payload = self.typed[1]
|
|
320
|
+
from ..contract_screen import thread_title, visible_recipient
|
|
321
|
+
recipient = (visible_recipient(self.agent._masked(snapshot), app_name, element)
|
|
322
|
+
if act in policy.MESSAGE_ACTS else None)
|
|
323
|
+
# One message, one approval: a Return that didn't send isn't asked again for the Send tap after it.
|
|
324
|
+
held = self.approved_send
|
|
325
|
+
if (held and op in ("TAP", "LONG_PRESS") and act in policy.MESSAGE_ACTS and payload == held["text"]
|
|
326
|
+
and held["app"] == app_name and held["thread"] == recipient):
|
|
327
|
+
self.approved_send = None
|
|
328
|
+
return element, snapshot, True
|
|
329
|
+
x, y, w, h = element.rect
|
|
330
|
+
request = {
|
|
331
|
+
"kind": "commit", "step": self.steps + 1, "operation": op, "label": label[:200], "role": role,
|
|
332
|
+
"text": self.agent._mask(payload) if payload else None,
|
|
333
|
+
"title": title or policy.approval_title(act, recipient=recipient), "act": act, "app_name": app_name,
|
|
334
|
+
"target": {"x": round(x, 4), "y": round(y, 4), "w": round(w, 4), "h": round(h, 4)}}
|
|
335
|
+
if act in offers.MONEY_ACTS:
|
|
336
|
+
reason = offers.reason(self.agent._masked(snapshot), label, reason) # what it buys, as the screen shows it
|
|
337
|
+
if reason:
|
|
338
|
+
request["reason"] = reason
|
|
339
|
+
if remember:
|
|
340
|
+
request["remember"] = remember
|
|
341
|
+
from ..frontier import CHANGED_WHILE_ASKING, FrontierAgent, binding, same_binding
|
|
342
|
+
bound = binding(snapshot, element)
|
|
343
|
+
answer = asker(request)
|
|
344
|
+
answer = str(answer or "denied")
|
|
345
|
+
if answer == "approved":
|
|
346
|
+
# The approval is bound to what the card showed: a fresh read must still show it.
|
|
347
|
+
fresh = self._read()
|
|
348
|
+
found = FrontierAgent._find(fresh, element.label, element.role, element.locator)
|
|
349
|
+
if found is None or not same_binding(bound, binding(fresh, found)):
|
|
350
|
+
self._show(fresh)
|
|
351
|
+
raise ToolProblem(CHANGED_WHILE_ASKING + " Call screen, then make the call again.")
|
|
352
|
+
# Return in a composer is a message whatever its label calls it ("Text Alex" names no send).
|
|
353
|
+
thread = recipient or thread_title(self.agent._masked(snapshot), app_name)
|
|
354
|
+
self.approved_send = ({"text": payload, "app": app_name, "thread": thread}
|
|
355
|
+
if op in ("TYPE_SUBMIT", "SUBMIT") and payload else None)
|
|
356
|
+
return found, fresh, True
|
|
357
|
+
self._declined(answer, label)
|
|
358
|
+
|
|
359
|
+
def _link_approval(self, decision):
|
|
360
|
+
"""A link deeplinks.decide says asks."""
|
|
361
|
+
ask = self.approve or self.ask
|
|
362
|
+
if ask is None:
|
|
363
|
+
raise ToolProblem("Nobody is watching this task to approve that link, so Mobster didn't open it. Open "
|
|
364
|
+
"the app and use its screens instead, or finish and say what you need.")
|
|
365
|
+
snapshot = self.snapshot if self.snapshot is not None else self._read()
|
|
366
|
+
url = decision.url
|
|
367
|
+
request = {"kind": "commit", "step": self.steps + 1, "operation": "OPEN_URL", "label": f"Open {url[:180]}",
|
|
368
|
+
"role": "Link", "text": self.agent._mask(url[:500]), "title": decision.title or "Open this link?",
|
|
369
|
+
"act": "open link", "app_name": decision.app_name or self.agent._app_name(snapshot) or None}
|
|
370
|
+
if decision.remember() and self.remembered is not None: # never for a run an outside agent started
|
|
371
|
+
request["remember"] = decision.remember()
|
|
372
|
+
self._declined(str(ask(request) or "denied"), url[:200])
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _declined(answer, label):
|
|
376
|
+
"""Raises for any answer but approved: what the agent is told when the person said no, or nobody did."""
|
|
377
|
+
if answer == "approved":
|
|
378
|
+
return
|
|
379
|
+
if answer == "stopped":
|
|
380
|
+
raise RunStopped("The person stopped this task. Stop now and say nothing more.")
|
|
381
|
+
if answer.startswith("redirected:"):
|
|
382
|
+
raise ToolProblem(f"The person declined that and said what to do instead: {answer.split(':', 1)[1]}")
|
|
383
|
+
if answer == "timeout":
|
|
384
|
+
raise ToolProblem("Nobody answered the approval in time, so Mobster didn't do it. Finish, and say it "
|
|
385
|
+
"is still waiting for their OK.")
|
|
386
|
+
raise ToolProblem(f"The person declined {json.dumps(label or 'that')}, so Mobster didn't do it. Don't try "
|
|
387
|
+
"another way: finish and say so, or ask them with ask_user.")
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _refused(snapshot):
|
|
391
|
+
"""The iPhone's own passcode keypad is never touched: Mobster doesn't unlock phones."""
|
|
392
|
+
from ..frontier import KEYPAD_REFUSAL
|
|
393
|
+
from ..task_policy import springboard_keypad
|
|
394
|
+
if springboard_keypad(snapshot):
|
|
395
|
+
raise ToolProblem(KEYPAD_REFUSAL.replace("Refused:", "Mobster refused this:", 1))
|
|
396
|
+
|
|
397
|
+
def _once(self, kind, execute, finish):
|
|
398
|
+
"""Send one action that must not be sent twice, then ``finish`` it; once its first byte may have left, any
|
|
399
|
+
failure ends the run with the outcome unknown, and nothing here retries it."""
|
|
400
|
+
from ..agent_hooks import GuardVerdict
|
|
401
|
+
from ..wda.guards import DriverRejection
|
|
402
|
+
sent = False
|
|
403
|
+
try:
|
|
404
|
+
before = execute()
|
|
405
|
+
sent = True
|
|
406
|
+
self.actions += 1
|
|
407
|
+
return finish(before)
|
|
408
|
+
except RunStopped:
|
|
409
|
+
raise
|
|
410
|
+
except Exception as error: # noqa: BLE001 -- anything after dispatch is an unknown outcome
|
|
411
|
+
if not sent and isinstance(error, DriverRejection) and error.code in DriverRejection.PRE_DISPATCH:
|
|
412
|
+
raise
|
|
413
|
+
self.stopped = GuardVerdict("stop", "action_unknown", f"The {kind} outcome is unknown. Inspect the phone "
|
|
414
|
+
"before starting another task; it was not retried.")
|
|
415
|
+
raise RunStopped(self.stopped.message) from None
|
|
416
|
+
|
|
417
|
+
def _do(self, op, element, snapshot, *, text=None, app=None, step_label=None, field=None, where=None,
|
|
418
|
+
image=False, point=None, asks=False):
|
|
419
|
+
"""One action through the frontier's _execute, then the step (with its frame) and the screen after."""
|
|
420
|
+
from ..frontier import UNKNOWN_BLOCKS
|
|
421
|
+
from ..frontier.stages.after import may_commit
|
|
422
|
+
from ..smart_events import PRESSES, step_text
|
|
423
|
+
self._check_running()
|
|
424
|
+
label = step_label if step_label is not None else (element.label if element is not None else "")
|
|
425
|
+
self.out({"event": "action_started", "operation": op, "target": label})
|
|
426
|
+
|
|
427
|
+
def finish(before):
|
|
428
|
+
if op in GESTURE_OPS:
|
|
429
|
+
self.out({"event": "action_acknowledged", "step": self.steps + 1, "operation": op})
|
|
430
|
+
after = self.agent._settle(before, last=True)
|
|
431
|
+
self.agent._check_app(after)
|
|
432
|
+
self.agent.last_screen = after
|
|
433
|
+
words = step_text(op, label, self.apps, text=text, field=field, where=where)
|
|
434
|
+
changed = getattr(before, "content_fingerprint", None) != getattr(after, "content_fingerprint", None)
|
|
435
|
+
# A press commits by the permission gate's reading or by the loop's own test, where a test that fails counts
|
|
436
|
+
# as a commit (may_commit), so a tap that worked never ends the run as an unknown outcome.
|
|
437
|
+
self._step(words, commit=asks or may_commit(op, element, snapshot) if op in PRESSES else None,
|
|
438
|
+
changed=changed)
|
|
439
|
+
feedback, self.agent.run_state.macro_feedback = self.agent.run_state.macro_feedback, None
|
|
440
|
+
head = words + "." + ("" if changed or op in ("READ_LIST",) else " The screen didn't change.")
|
|
441
|
+
if feedback:
|
|
442
|
+
head += " " + feedback
|
|
443
|
+
result = self._show(after, image=image, head=head)
|
|
444
|
+
result.no_effect = self._no_effect(op, app, text, after, changed) or ""
|
|
445
|
+
return result
|
|
446
|
+
|
|
447
|
+
def execute():
|
|
448
|
+
self.agent.run_state.last_receipt = None
|
|
449
|
+
return self.agent._execute(op, element, app, text, snapshot, point=point)
|
|
450
|
+
|
|
451
|
+
if op in UNKNOWN_BLOCKS or op in GESTURE_OPS:
|
|
452
|
+
return self._once(GESTURE_OPS.get(op) or ("tap" if op in ("TAP", "TAP_XY", "LONG_PRESS") else "action"),
|
|
453
|
+
execute, finish)
|
|
454
|
+
before = execute()
|
|
455
|
+
self.actions += 1
|
|
456
|
+
return finish(before)
|
|
457
|
+
|
|
458
|
+
def _no_effect(self, op, app, text, after, changed):
|
|
459
|
+
"""Why the read after an action shows no effect, for act's chain, or None when it shows one. The screen moving
|
|
460
|
+
is the plain case. An app in front, an item on screen and a write whose field reads back are effects without a
|
|
461
|
+
move, and a scroll that moved without reaching its item isn't one. A moved screen isn't proof of the right
|
|
462
|
+
screen: this only stops a chain from running on past a step that did nothing it can see."""
|
|
463
|
+
if op == "LAUNCH_APP":
|
|
464
|
+
return None if after.bundle_id == app else "the app isn't in front"
|
|
465
|
+
if op == "HOME":
|
|
466
|
+
return None if after.bundle_id == "com.apple.springboard" else "the Home Screen isn't showing"
|
|
467
|
+
if op == "SCROLL_TO":
|
|
468
|
+
return None if (text or "").casefold() in (after.text or "").casefold() else "the item isn't on screen"
|
|
469
|
+
receipt = self.agent.run_state.last_receipt
|
|
470
|
+
if receipt is not None and receipt.get("value") is not None:
|
|
471
|
+
return None if receipt.get("matches") else "the field doesn't hold what was written"
|
|
472
|
+
return None if changed else "the screen didn't change"
|
|
473
|
+
|
|
474
|
+
def _step(self, words, commit=None, changed=None):
|
|
475
|
+
self.steps += 1
|
|
476
|
+
event = {"event": "step", "n": self.steps, "step": self.steps, "text": words}
|
|
477
|
+
# Left out when unknown (smart_events.SmartEvents._step).
|
|
478
|
+
if isinstance(commit, bool):
|
|
479
|
+
event["commit"] = commit
|
|
480
|
+
if isinstance(changed, bool):
|
|
481
|
+
event["changed"] = changed
|
|
482
|
+
data = frame_bytes(self.driver)
|
|
483
|
+
if data:
|
|
484
|
+
event["_frame"] = data
|
|
485
|
+
self.out(event)
|
|
486
|
+
|
|
487
|
+
def _field(self, element):
|
|
488
|
+
from ..contract_screen import COMPOSER, SEARCHY
|
|
489
|
+
words = f"{element.label} {element.placeholder}"
|
|
490
|
+
if element.role == "SearchField" or SEARCHY.search(words):
|
|
491
|
+
return "search"
|
|
492
|
+
return "composer" if COMPOSER.search(words) else "field"
|
|
493
|
+
|
|
494
|
+
def tool_screen(self, a):
|
|
495
|
+
return self._show(self._read(), image=a.get("image", True))
|
|
496
|
+
|
|
497
|
+
def tool_tap(self, a):
|
|
498
|
+
element, snapshot = self._element(a)
|
|
499
|
+
self._refused(snapshot)
|
|
500
|
+
element, snapshot, asks = self._approval("TAP", element, snapshot, None, bool(a.get("irreversible")))
|
|
501
|
+
return self._do("TAP", element, snapshot, image=a.get("image", False), asks=asks)
|
|
502
|
+
|
|
503
|
+
def _point_approval(self, operation, points, label, snapshot):
|
|
504
|
+
"""Ask before a tap at a point or a gesture, as the gate reads it; a block at any point refuses the whole
|
|
505
|
+
action, and what asks is refused with nobody to answer."""
|
|
506
|
+
from ..contract_asks import point_spot
|
|
507
|
+
from ..permissions import action_from
|
|
508
|
+
masked = self.agent._masked(snapshot)
|
|
509
|
+
app_name = self.agent._app_name(snapshot) or ""
|
|
510
|
+
decision = None
|
|
511
|
+
for location in points:
|
|
512
|
+
_, under = point_spot(snapshot, *location)
|
|
513
|
+
spot = under or SimpleNamespace(label="", role="Point", value="", locator="", placeholder="",
|
|
514
|
+
identifier="", rect=(*location, .001, .001))
|
|
515
|
+
decision = self.permissions.decide(action_from("TAP_XY", spot, masked, app_name=app_name),
|
|
516
|
+
existing_act="other" if operation != "TAP_XY" else None)
|
|
517
|
+
if decision.decision == "block":
|
|
518
|
+
raise ToolProblem(f"Refused: {decision.reason or 'not allowed'}. Nothing was done.")
|
|
519
|
+
if operation != "TAP_XY" or decision.act:
|
|
520
|
+
self.changes += 1 # a gesture, or a point on something that acts
|
|
521
|
+
if decision.decision == "allow":
|
|
522
|
+
return False
|
|
523
|
+
asker = self.approve or self.ask
|
|
524
|
+
if asker is None:
|
|
525
|
+
raise ToolProblem(f"{json.dumps(label or 'That')} needs the person's OK, and nobody is watching this task "
|
|
526
|
+
"to give it, so Mobster didn't do it. Finish and say what you need.")
|
|
527
|
+
x, y = points[0]
|
|
528
|
+
request = {"kind": "commit", "step": self.steps + 1, "operation": operation, "label": label[:200],
|
|
529
|
+
"role": "Point", "text": None, "act": decision.act or "other", "app_name": app_name or None,
|
|
530
|
+
"title": (decision.title or f"{label[:60]}?" if operation == "TAP_XY"
|
|
531
|
+
else f"{operation[:-3].capitalize()} {label[:60]}? This may change something."),
|
|
532
|
+
"target": {"x": round(x, 4), "y": round(y, 4), "w": .001, "h": .001}}
|
|
533
|
+
if decision.reason:
|
|
534
|
+
request["reason"] = decision.reason
|
|
535
|
+
self._declined(str(asker(request) or "denied"), label)
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
def tool_tap_point(self, a):
|
|
539
|
+
x, y, label = a.get("x"), a.get("y"), a.get("label")
|
|
540
|
+
if (any(isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or not 0 < v < 100
|
|
541
|
+
for v in (x, y)) or not isinstance(label, str) or not label.strip()):
|
|
542
|
+
raise ToolProblem("Name the visible control in label and give x and y between 0 and 100 percent.")
|
|
543
|
+
self._point_image_required()
|
|
544
|
+
|
|
545
|
+
def grounded():
|
|
546
|
+
try:
|
|
547
|
+
fresh = self.agent.point_screen(self.snapshot, self.point_image, [(x / 100, y / 100)])
|
|
548
|
+
except ValueError as error:
|
|
549
|
+
raise ToolProblem(str(error)) from None
|
|
550
|
+
self._refused(fresh)
|
|
551
|
+
return fresh
|
|
552
|
+
|
|
553
|
+
from ..frontier import Action
|
|
554
|
+
snapshot = grounded()
|
|
555
|
+
point, refused = self.agent._tap_xy(self.goal, Action("TAP_XY", target=f"{x}%,{y}%", text=label), self.aliases,
|
|
556
|
+
snapshot)
|
|
557
|
+
if refused:
|
|
558
|
+
raise ToolProblem(refused)
|
|
559
|
+
if self._point_approval("TAP_XY", [point], label, snapshot):
|
|
560
|
+
snapshot = grounded() # the person approved the spot on the screen as it was
|
|
561
|
+
self._check_running()
|
|
562
|
+
self.point_image = None
|
|
563
|
+
return self._do("TAP_XY", None, snapshot, point=point, step_label=label, image=True)
|
|
564
|
+
|
|
565
|
+
def _point_image_required(self):
|
|
566
|
+
if self.snapshot is None or self.point_image is None:
|
|
567
|
+
raise ToolProblem("Call screen with image true before choosing a point on the screenshot.")
|
|
568
|
+
|
|
569
|
+
def _point_gesture(self, kind, arguments):
|
|
570
|
+
self._point_image_required()
|
|
571
|
+
try:
|
|
572
|
+
gesture = validate_gesture_request(kind, arguments)
|
|
573
|
+
snapshot = self.agent.gesture_screen(self.snapshot, self.point_image, gesture, self.goal)
|
|
574
|
+
except ValueError as error:
|
|
575
|
+
raise ToolProblem(str(error)) from None
|
|
576
|
+
self._refused(snapshot)
|
|
577
|
+
if self._point_approval(gesture.operation, gesture.points, gesture.label, snapshot):
|
|
578
|
+
try:
|
|
579
|
+
snapshot = self.agent.gesture_screen(self.snapshot, self.point_image, gesture, self.goal)
|
|
580
|
+
except ValueError as error:
|
|
581
|
+
raise ToolProblem(str(error)) from None
|
|
582
|
+
self._check_running()
|
|
583
|
+
self.point_image = None # a gesture consumes the picture even if reading its result fails
|
|
584
|
+
return self._do(gesture.operation, None, snapshot, point=gesture, step_label=gesture.label, image=True)
|
|
585
|
+
|
|
586
|
+
def tool_drag_point(self, a):
|
|
587
|
+
return self._point_gesture("drag", a)
|
|
588
|
+
|
|
589
|
+
def tool_hold_point(self, a):
|
|
590
|
+
return self._point_gesture("hold", a)
|
|
591
|
+
|
|
592
|
+
def tool_flick_point(self, a):
|
|
593
|
+
return self._point_gesture("flick", a)
|
|
594
|
+
|
|
595
|
+
def tool_long_press(self, a):
|
|
596
|
+
element, snapshot = self._element(a)
|
|
597
|
+
self._refused(snapshot)
|
|
598
|
+
element, snapshot, asks = self._approval("LONG_PRESS", element, snapshot, None, bool(a.get("irreversible")))
|
|
599
|
+
return self._do("LONG_PRESS", element, snapshot, image=a.get("image", False), asks=asks)
|
|
600
|
+
|
|
601
|
+
def tool_type_text(self, a):
|
|
602
|
+
text = a.get("text")
|
|
603
|
+
if not isinstance(text, str) or not text:
|
|
604
|
+
raise ToolProblem("Say what to type in text.")
|
|
605
|
+
element, snapshot = self._element(a)
|
|
606
|
+
self._refused(snapshot)
|
|
607
|
+
if not element.editable:
|
|
608
|
+
raise ToolProblem(f"{json.dumps(element.label or 'That element')} isn't a text field. Tap the field "
|
|
609
|
+
"first, or name the field itself.")
|
|
610
|
+
from ..state import validate_input_text
|
|
611
|
+
try: # refused here, where it can be corrected: past this point a failed send ends the run
|
|
612
|
+
validate_input_text(text, multiline=element.role == "TextView")
|
|
613
|
+
except ValueError as error:
|
|
614
|
+
raise ToolProblem(f"{error}. Shorten or correct the text and type it again.") from None
|
|
615
|
+
submit, append = bool(a.get("submit")), bool(a.get("append"))
|
|
616
|
+
field = self._field(element)
|
|
617
|
+
if field == "composer":
|
|
618
|
+
current = (element.value or "") if append and has_text(element) else ""
|
|
619
|
+
self.typed = (self.agent._app_name(snapshot) or None, current + text)
|
|
620
|
+
# The permission gate reads what is typed (a password is never typed); Return asks as a commit.
|
|
621
|
+
element, snapshot, _ = self._approval("TYPE_SUBMIT" if submit else "TYPE", element, snapshot, text,
|
|
622
|
+
bool(a.get("irreversible")))
|
|
623
|
+
if submit and (append or not has_text(element)):
|
|
624
|
+
return self._do("TYPE_SUBMIT", element, snapshot, text=text, field=field, image=a.get("image", False))
|
|
625
|
+
# An empty field takes the text as it is: no clear first (a clear and a read cost a round trip).
|
|
626
|
+
op = "TYPE" if append or not has_text(element) else "SET_TEXT"
|
|
627
|
+
result = self._do(op, element, snapshot, text=text, field=field)
|
|
628
|
+
if not submit:
|
|
629
|
+
return result
|
|
630
|
+
from ..frontier import FrontierAgent
|
|
631
|
+
current = self.snapshot
|
|
632
|
+
again = FrontierAgent._find(current, element.label, element.role, element.locator) or element
|
|
633
|
+
return self._do("SUBMIT", again, current, step_label=element.label, image=a.get("image", False))
|
|
634
|
+
|
|
635
|
+
def tool_swipe(self, a):
|
|
636
|
+
direction = a.get("direction")
|
|
637
|
+
if direction not in SWIPES:
|
|
638
|
+
raise ToolProblem("direction is up, down, left or right (up shows what is below).")
|
|
639
|
+
element, snapshot = self._element(a, required=False)
|
|
640
|
+
op = SWIPES[direction]
|
|
641
|
+
if op in ("SWIPE_UP", "SWIPE_DOWN"):
|
|
642
|
+
element = None # the frontier scrolls the screen's scroll view
|
|
643
|
+
elif element is not None:
|
|
644
|
+
# A row's swipe archives or deletes in some apps (Gmail, Outlook): the gate reads it like a tap.
|
|
645
|
+
element, snapshot, _ = self._approval(op, element, snapshot, None, bool(a.get("irreversible")))
|
|
646
|
+
return self._do(op, element, snapshot, image=a.get("image", False))
|
|
647
|
+
|
|
648
|
+
def tool_open_app(self, a):
|
|
649
|
+
wanted = " ".join(str(a.get("app") or "").split())
|
|
650
|
+
if not wanted:
|
|
651
|
+
raise ToolProblem("Say which app: its name as on the Home Screen, or its bundle id.")
|
|
652
|
+
bundle = wanted if wanted in self.apps else next(
|
|
653
|
+
(b for b, name in self.apps.items() if name.casefold() == wanted.casefold()), None)
|
|
654
|
+
if bundle is None:
|
|
655
|
+
near = [name for name in self.apps.values() if wanted.casefold() in name.casefold()][:5]
|
|
656
|
+
raise ToolProblem(f"There's no app called {json.dumps(wanted)} on this iPhone."
|
|
657
|
+
+ (" Did you mean " + ", ".join(near) + "?" if near else
|
|
658
|
+
" The apps are listed in your instructions."))
|
|
659
|
+
snapshot = self.snapshot if self.snapshot is not None else self._read()
|
|
660
|
+
self._allowed_app(bundle) # asks before the app opens, the first time
|
|
661
|
+
return self._do("LAUNCH_APP", None, snapshot, app=bundle, step_label=bundle, image=a.get("image", False))
|
|
662
|
+
|
|
663
|
+
def _allowed_app(self, bundle):
|
|
664
|
+
"""Raises ToolProblem unless the person lets Mobster use ``bundle`` (the run's guard asks the first time)."""
|
|
665
|
+
from ..agent_hooks import safe_allows
|
|
666
|
+
guard = getattr(self.agent, "guard", None)
|
|
667
|
+
if not bundle or bundle == "com.apple.springboard" or guard is None or safe_allows(guard, bundle):
|
|
668
|
+
return
|
|
669
|
+
said = getattr(guard, "blocked_message", None)
|
|
670
|
+
try:
|
|
671
|
+
why = said(bundle, self.apps.get(bundle)) if callable(said) else None
|
|
672
|
+
except Exception:
|
|
673
|
+
why = None
|
|
674
|
+
raise ToolProblem((why or f"Mobster may not use {self.apps.get(bundle, bundle)}.") + " Do the task another "
|
|
675
|
+
"way, or finish and say what is left for the person.")
|
|
676
|
+
|
|
677
|
+
def tool_home(self, a):
|
|
678
|
+
snapshot = self.snapshot if self.snapshot is not None else self._read()
|
|
679
|
+
return self._do("HOME", None, snapshot, image=a.get("image", False))
|
|
680
|
+
|
|
681
|
+
def tool_dismiss(self, a):
|
|
682
|
+
snapshot = self._read()
|
|
683
|
+
return self._do("DISMISS", None, snapshot)
|
|
684
|
+
|
|
685
|
+
def tool_read_list(self, a):
|
|
686
|
+
snapshot = self._read()
|
|
687
|
+
stop_at = a.get("stop_at") or None
|
|
688
|
+
app = self.agent._app_name(snapshot) or None
|
|
689
|
+
self.out({"event": "action_started", "operation": "READ_LIST", "target": ""})
|
|
690
|
+
self.agent._execute("READ_LIST", None, None, stop_at, snapshot)
|
|
691
|
+
rows, self.agent.run_state.listing = list(self.agent.run_state.listing or ()), None
|
|
692
|
+
from ..smart_events import step_text
|
|
693
|
+
self.actions += 1
|
|
694
|
+
self._step(step_text("READ_LIST", "", self.apps, where=app))
|
|
695
|
+
after = self.agent._read_once()
|
|
696
|
+
feedback, self.agent.run_state.macro_feedback = self.agent.run_state.macro_feedback, None
|
|
697
|
+
shown = rows[:LIST_ROWS_SHOWN]
|
|
698
|
+
head = (f"Read {len(rows)} row{'s' if len(rows) != 1 else ''} of the list, top to bottom"
|
|
699
|
+
+ (f" (showing the first {LIST_ROWS_SHOWN})" if len(rows) > LIST_ROWS_SHOWN else "") + ":\n"
|
|
700
|
+
+ "\n".join(f"- {self.agent._mask(row)}" for row in shown) + ("\n" + feedback if feedback else ""))
|
|
701
|
+
return self._show(after, head=head)
|
|
702
|
+
|
|
703
|
+
def tool_scroll_to(self, a):
|
|
704
|
+
text = " ".join(str(a.get("text") or "").split())
|
|
705
|
+
if not text:
|
|
706
|
+
raise ToolProblem("Say the text of the item to scroll to.")
|
|
707
|
+
snapshot = self._read()
|
|
708
|
+
return self._do("SCROLL_TO", None, snapshot, text=text, step_label="")
|
|
709
|
+
|
|
710
|
+
def tool_open_url(self, a):
|
|
711
|
+
url = str(a.get("url") or "").strip()
|
|
712
|
+
if not re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:", url) or len(url) > 2000:
|
|
713
|
+
raise ToolProblem("The link needs a scheme, such as https://example.com or maps://?q=coffee.")
|
|
714
|
+
self._refused(self.snapshot if self.snapshot is not None else self._read())
|
|
715
|
+
from ..deeplinks import LinkError, app_for, decide, open_link
|
|
716
|
+
try:
|
|
717
|
+
decision = decide(url, self.remembered)
|
|
718
|
+
except LinkError as error:
|
|
719
|
+
raise ToolProblem(str(error)) from None
|
|
720
|
+
self._allowed_app(app_for(url)) # the app the link opens, before it opens
|
|
721
|
+
if decision.action == "ask":
|
|
722
|
+
self.changes += 1
|
|
723
|
+
self._link_approval(decision)
|
|
724
|
+
self.out({"event": "action_started", "operation": "OPEN_URL", "target": url[:80]})
|
|
725
|
+
opener = getattr(self.driver, "open_url", None)
|
|
726
|
+
if callable(opener):
|
|
727
|
+
opener(url)
|
|
728
|
+
else:
|
|
729
|
+
open_link(self.driver, url, bundle=app_for(url) or None)
|
|
730
|
+
self.actions += 1
|
|
731
|
+
words = self._link_words(url)
|
|
732
|
+
self._step(words)
|
|
733
|
+
return self._show(self._read(), image=a.get("image", False), head=f"Opened {url[:200]}.")
|
|
734
|
+
|
|
735
|
+
def _link_words(self, url):
|
|
736
|
+
"""A link's step in plain words, never its scheme or query: "Opened example.com", "Opened Settings", else
|
|
737
|
+
"Opened a link"."""
|
|
738
|
+
from urllib.parse import urlsplit
|
|
739
|
+
|
|
740
|
+
from ..catalog import APPS
|
|
741
|
+
from ..deeplinks import app_for
|
|
742
|
+
host = urlsplit(url).hostname if url.casefold().startswith(("http://", "https://")) else None
|
|
743
|
+
if host:
|
|
744
|
+
return f"Opened {host.removeprefix('www.')[:60]}"
|
|
745
|
+
bundle = app_for(url)
|
|
746
|
+
name = self.apps.get(bundle) or next((app["name"] for app in APPS if app["bundleId"] == bundle), None)
|
|
747
|
+
return f"Opened {name}" if name else "Opened a link"
|
|
748
|
+
|
|
749
|
+
def tool_wait(self, a):
|
|
750
|
+
try:
|
|
751
|
+
seconds = min(WAIT_MAX, max(0.5, float(a.get("seconds", 2))))
|
|
752
|
+
except (TypeError, ValueError):
|
|
753
|
+
seconds = 2.0
|
|
754
|
+
deadline = time.monotonic() + seconds
|
|
755
|
+
while time.monotonic() < deadline:
|
|
756
|
+
if self.cancelled():
|
|
757
|
+
raise RunStopped("The person stopped this task. Stop now and say nothing more.")
|
|
758
|
+
time.sleep(min(.25, max(0.0, deadline - time.monotonic())))
|
|
759
|
+
return self._show(self._read(), head=f"Waited {seconds:g} s.")
|
|
760
|
+
|
|
761
|
+
def tool_ask_user(self, a):
|
|
762
|
+
question = " ".join(str(a.get("question") or "").split())[:QUESTION_CHARS]
|
|
763
|
+
if not question:
|
|
764
|
+
raise ToolProblem("Say what to ask in question.")
|
|
765
|
+
if self.ask is None:
|
|
766
|
+
raise ToolProblem("Nobody is watching this task to answer. Decide from the request, or finish and say "
|
|
767
|
+
"what you need to know.")
|
|
768
|
+
choices = [" ".join(str(c).split())[:CHOICE_CHARS] for c in (a.get("choices") or ()) if str(c).strip()][:6]
|
|
769
|
+
request = {"kind": "clarify", "operation": "ASK_USER", "label": question[:80], "question": question,
|
|
770
|
+
"choices": [{"id": f"c{i + 1}", "label": c} for i, c in enumerate(choices)],
|
|
771
|
+
"allow_text": True, "step": self.steps + 1}
|
|
772
|
+
answer = str(self.ask(request) or "timeout")
|
|
773
|
+
if answer == "stopped":
|
|
774
|
+
raise RunStopped("The person stopped this task. Stop now and say nothing more.")
|
|
775
|
+
if answer.startswith("answer:"):
|
|
776
|
+
return ToolResult("They answered: " + answer.split(":", 1)[1], {"answer": answer.split(":", 1)[1]})
|
|
777
|
+
if answer.startswith("choice:"):
|
|
778
|
+
picked = answer.split(":", 1)[1]
|
|
779
|
+
label = next((c for i, c in enumerate(choices) if f"c{i + 1}" == picked), picked)
|
|
780
|
+
return ToolResult("They chose: " + label, {"answer": label})
|
|
781
|
+
if answer == "approved" and choices:
|
|
782
|
+
return ToolResult("They chose: " + choices[0], {"answer": choices[0]})
|
|
783
|
+
raise ToolProblem("Nobody answered in time. Finish, and say what you need to know.")
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _schema(properties, required=()):
|
|
787
|
+
return {"type": "object", "properties": properties, "required": list(required), "additionalProperties": False}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
ELEMENT = {"type": "string", "pattern": ELEMENT_ID.pattern,
|
|
791
|
+
"description": "the element's id from the latest screen, such as e12"}
|
|
792
|
+
LABEL = {"type": "string", "minLength": 1, "maxLength": 200,
|
|
793
|
+
"description": "or the element's visible label, exactly as the screen lists it"}
|
|
794
|
+
IMAGE = {"type": "boolean",
|
|
795
|
+
"description": "also return a screenshot (default false: the text screen is usually enough)"}
|
|
796
|
+
IRREVERSIBLE = {"type": "boolean",
|
|
797
|
+
"description": "true when this sends, posts, buys, books, deletes or can't be undone: "
|
|
798
|
+
"Mobster asks the person first"}
|
|
799
|
+
DESCRIPTIONS = {
|
|
800
|
+
"hold_point": "Hold a named visible screenshot point for 0.1 to 10 seconds (default 1.5), then release. Uses the "
|
|
801
|
+
"screenshot from your last screen call, existing approvals and one bounded gesture. Returns a fresh "
|
|
802
|
+
"screenshot; cannot be chained or interrupted during contact. An unknown outcome stops the run.",
|
|
803
|
+
"flick_point": "Flick between named visible screenshot points in 0.08 to 0.3 seconds (default 0.12), without a "
|
|
804
|
+
"pickup hold. Existing approvals and screenshot and path checks apply. Returns a fresh screenshot; "
|
|
805
|
+
"cannot be chained. An unknown outcome stops the run.",
|
|
806
|
+
"drag_point": "Drag between two visible screenshot points with named endpoints and screen percentages. Optionally "
|
|
807
|
+
"hold before moving with hold_seconds (0 to 2, default 0.1). Uses one gesture, returns a fresh "
|
|
808
|
+
"screenshot and cannot be chained. Existing approval rules apply; a changed screen, or a sensitive "
|
|
809
|
+
"control along the path, is refused. An unknown outcome stops the run.",
|
|
810
|
+
"tap_point": "Tap a visible control that has no element id, such as an icon the screen listing leaves out. x and y "
|
|
811
|
+
"are screen percentages from your last screenshot, label names the control. Returns a fresh "
|
|
812
|
+
"screenshot; choose each point from it. Existing approval rules still apply.",
|
|
813
|
+
"act": "Do several actions in a row in one call when you can predict the screens, such as opening a chat, typing "
|
|
814
|
+
"and tapping Send: faster than one call per action. Name elements after the first by label, since the "
|
|
815
|
+
"screen changes. Stops at the first step that fails, isn't approved or shows no effect, "
|
|
816
|
+
"and returns the screen after the last.",
|
|
817
|
+
"screen": "Read the iPhone's current screen: the app in front and every element with an id (e1, e2 ...), its role, "
|
|
818
|
+
"label, value and position, plus a screenshot. Call it first, and whenever you are unsure what is shown.",
|
|
819
|
+
"tap": "Tap an element, named by its id from the latest screen or by its label. Returns the screen after the tap. "
|
|
820
|
+
"Mobster asks the person before a tap that sends, posts, buys, books or deletes; "
|
|
821
|
+
"mark such a tap irreversible.",
|
|
822
|
+
"type_text": "Type into a text field (replacing its text unless append is true); submit presses Return after "
|
|
823
|
+
"typing. Returns the screen after. Return that sends a message waits for the person's OK.",
|
|
824
|
+
"swipe": "Swipe the screen: up shows what is below, down what is above; left and right on an element swipe that "
|
|
825
|
+
"row or carousel. Returns the screen after.",
|
|
826
|
+
"long_press": ("Press and hold an element (opens its menu). Returns the screen after. Mobster asks the person "
|
|
827
|
+
"first when the hold commits something, as for tap."),
|
|
828
|
+
"open_app": "Open an app by its name as on the Home Screen (Messages, Notes) or its bundle id. Faster and surer "
|
|
829
|
+
"than finding its icon. Returns the screen after.",
|
|
830
|
+
"home": "Go to the Home Screen.",
|
|
831
|
+
"read_list": "Read the whole list or feed on screen from here down in one call, scrolling as it goes (up to about "
|
|
832
|
+
"60 rows): faster than swiping and reading screen by screen. stop_at stops at the first row "
|
|
833
|
+
"containing that text.",
|
|
834
|
+
"scroll_to": "Scroll down until an item containing this text is on screen. Returns the screen after.",
|
|
835
|
+
"dismiss": "Close a pop-up, sheet or banner that covers the screen, by tapping outside it.",
|
|
836
|
+
"open_url": ("Open a link or deep link on the iPhone (https://..., maps://?q=..., tel:, sms:). A web page, Maps or "
|
|
837
|
+
"the App Store opens at once; a call, FaceTime, a message, an email or a shortcut waits for the "
|
|
838
|
+
"person's OK each time, and another app's link the first time."),
|
|
839
|
+
"ask_user": "Ask the person a question (with up to 6 choices) and wait for their answer. Use it when the "
|
|
840
|
+
"request is "
|
|
841
|
+
"ambiguous or you need something only they know; never for a password or passcode. A choice that names "
|
|
842
|
+
"a person or a place says what tells it apart after \" \u2014 \" (\"Sam Lee \u2014 texted Tuesday\").",
|
|
843
|
+
"wait": "Wait a few seconds (at most 10) for something to load, then read the screen.",
|
|
844
|
+
}
|
|
845
|
+
SCHEMAS = {
|
|
846
|
+
"drag_point": DRAG_SCHEMA,
|
|
847
|
+
"hold_point": HOLD_SCHEMA,
|
|
848
|
+
"flick_point": FLICK_SCHEMA,
|
|
849
|
+
"tap_point": _schema({"x": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100},
|
|
850
|
+
"y": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100},
|
|
851
|
+
"label": LABEL}, ("x", "y", "label")),
|
|
852
|
+
"screen": _schema({"image": {"type": "boolean", "description": "include a screenshot (default true)"}}),
|
|
853
|
+
"act": _schema({"steps": {"type": "array", "minItems": 1, "maxItems": MAX_CHAIN, "items": {
|
|
854
|
+
"type": "object", "required": ["tool"], "additionalProperties": True,
|
|
855
|
+
"properties": {"tool": {"type": "string", "enum": list(CHAINABLE)},
|
|
856
|
+
"element": ELEMENT, "label": LABEL,
|
|
857
|
+
"text": {"type": "string", "maxLength": 4000},
|
|
858
|
+
"submit": {"type": "boolean"}, "append": {"type": "boolean"},
|
|
859
|
+
"irreversible": IRREVERSIBLE,
|
|
860
|
+
"direction": {"type": "string", "enum": ["up", "down", "left", "right"]},
|
|
861
|
+
"app": {"type": "string", "maxLength": 200},
|
|
862
|
+
"seconds": {"type": "number", "minimum": 0.5, "maximum": WAIT_MAX}}},
|
|
863
|
+
"description": "the actions in order, each with its tool and that tool's arguments"}}, ("steps",)),
|
|
864
|
+
"tap": _schema({"element": ELEMENT, "label": LABEL, "irreversible": IRREVERSIBLE, "image": IMAGE}),
|
|
865
|
+
"type_text": _schema({"element": ELEMENT, "label": LABEL,
|
|
866
|
+
"text": {"type": "string", "minLength": 1, "maxLength": 4000, "description": "what to type"},
|
|
867
|
+
"submit": {"type": "boolean", "description": "press Return after typing"},
|
|
868
|
+
"append": {"type": "boolean",
|
|
869
|
+
"description": "add to the field's text instead of replacing it"},
|
|
870
|
+
"irreversible": IRREVERSIBLE, "image": IMAGE}, ("text",)),
|
|
871
|
+
"swipe": _schema({"direction": {"type": "string", "enum": ["up", "down", "left", "right"]},
|
|
872
|
+
"element": ELEMENT, "label": LABEL, "image": IMAGE}, ("direction",)),
|
|
873
|
+
"long_press": _schema({"element": ELEMENT, "label": LABEL, "irreversible": IRREVERSIBLE, "image": IMAGE}),
|
|
874
|
+
"open_app": _schema({"app": {"type": "string", "minLength": 1, "maxLength": 200,
|
|
875
|
+
"description": "the app's name or bundle id"}, "image": IMAGE}, ("app",)),
|
|
876
|
+
"home": _schema({"image": IMAGE}),
|
|
877
|
+
"read_list": _schema({"stop_at": {"type": "string", "maxLength": 200,
|
|
878
|
+
"description": "stop at the first row containing this text"}}),
|
|
879
|
+
"scroll_to": _schema({"text": {"type": "string", "minLength": 1, "maxLength": 200}}, ("text",)),
|
|
880
|
+
"dismiss": _schema({}),
|
|
881
|
+
"open_url": _schema({"url": {"type": "string", "minLength": 3, "maxLength": 2000}, "image": IMAGE}, ("url",)),
|
|
882
|
+
"ask_user": _schema({"question": {"type": "string", "minLength": 1, "maxLength": QUESTION_CHARS},
|
|
883
|
+
"choices": {"type": "array", "maxItems": 6,
|
|
884
|
+
"items": {"type": "string", "minLength": 1, "maxLength": CHOICE_CHARS}}},
|
|
885
|
+
("question",)),
|
|
886
|
+
"wait": _schema({"seconds": {"type": "number", "minimum": 0.5, "maximum": WAIT_MAX}}),
|
|
887
|
+
}
|
|
888
|
+
# The same risk hints as mobster mcp's tools (mcp_server/protocol.py): a tool that changes the phone is destructive.
|
|
889
|
+
READS = {**READ_ONLY, "idempotentHint": True}
|
|
890
|
+
ANNOTATIONS = {name: dict(READS if name in ("screen", "read_list", "wait", "ask_user") else ACTS) for name in NAMES}
|
|
891
|
+
for _name in NAMES:
|
|
892
|
+
ANNOTATIONS[_name]["title"] = _name.replace("_", " ").capitalize()
|