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,26 @@
|
|
|
1
|
+
"""The replay core: run the model once, record the exact action path, replay it with no model, heal as a diff.
|
|
2
|
+
|
|
3
|
+
One engine, two front ends (FEATURE-BACKLOG v2, item 1):
|
|
4
|
+
|
|
5
|
+
- **Checks** (`mobster test`): a passing Smart run records its actions with their tree anchors; later runs replay
|
|
6
|
+
them through WebDriverAgent with no model call. A step whose screen or target changed is healed by Smart for that
|
|
7
|
+
step only, and the change is written as a reviewable diff (``<check>.heal.yaml`` and ``healed[]`` in
|
|
8
|
+
results.json), never silently. A heal never changes an assertion: the verdict is still read from the tree.
|
|
9
|
+
- **Saved workflows** (the Mac app's Workflows): after the first good run a workflow is *learned* and runs without
|
|
10
|
+
the model. Typed text is never replayed blind (it comes from the workflow's own words, or the model writes it),
|
|
11
|
+
and sends, posts and deletes still stop for approval.
|
|
12
|
+
|
|
13
|
+
Modules:
|
|
14
|
+
|
|
15
|
+
- ``path.py``: the recorded path (steps with anchors) and its validation;
|
|
16
|
+
- ``recorder.py``: records a Smart run's executed actions (the frontier's step observer, plus the screens it read);
|
|
17
|
+
- ``engine.py``: replays a path on a driver: preconditions, actions, approvals, heals and the circuit breaker;
|
|
18
|
+
- ``store.py``: the per-machine cache, keyed by what the path replays (the check or workflow), the app version and
|
|
19
|
+
the iOS major version;
|
|
20
|
+
- ``heal.py``: heal entries, ``.heal.yaml`` files, Accept and Keep failing;
|
|
21
|
+
- ``checks.py``: the check front end (verify/runner.py calls it).
|
|
22
|
+
|
|
23
|
+
The workflow front end is part of Mobster for Mac: a pre-run hook and a run listener (harness_api) on this engine.
|
|
24
|
+
|
|
25
|
+
Nothing here calls a model itself: a heal is a callback the front end supplies.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""The check front end: `mobster test` replays a check's recorded path, heals it, and keeps the cache.
|
|
2
|
+
|
|
3
|
+
verify/runner.py's VerifyRun calls three things when a suite runs it with replay on:
|
|
4
|
+
|
|
5
|
+
- ``CheckReplay.recorder()`` while Smart runs: the frontier's actions become a path;
|
|
6
|
+
- ``CheckReplay.run(cancelled)`` instead of Smart when a path is cached: the replay, with Smart healing a step that
|
|
7
|
+
changed (for that step only) unless the run is strict (``--no-heal``);
|
|
8
|
+
- ``CheckReplay.after(result)`` once the verdict is in: what results.json says about the replay (``replay`` and
|
|
9
|
+
``healed``), and what is kept: a passing run's path goes to the cache, its heals to ``<check>.heal.yaml``.
|
|
10
|
+
|
|
11
|
+
Modes: ``auto`` replays and heals (the default of `mobster test`), ``strict`` replays and fails on a change
|
|
12
|
+
(``--no-heal``), ``fresh`` runs Smart and records (``--fresh``), ``off`` neither replays nor records.
|
|
13
|
+
|
|
14
|
+
The verdict never changes: replayed or not, healed or not, the assertions are read from the tree after the flow.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from .engine import STRONG, Heal, Replayer, match, screen_likeness
|
|
22
|
+
from .heal import HealError
|
|
23
|
+
from .path import describe, new_path, replayable_count
|
|
24
|
+
from .recorder import Recorder, literal_text
|
|
25
|
+
from .store import Cache, env_of, revision
|
|
26
|
+
|
|
27
|
+
MODES = ("auto", "strict", "fresh", "off")
|
|
28
|
+
HEAL_TURNS = 6
|
|
29
|
+
LOOKAHEAD = 4
|
|
30
|
+
# The same recorded step healed this often in one run stops the replay: the screen keeps undoing the heal.
|
|
31
|
+
HEALS_PER_STEP = 2
|
|
32
|
+
NOTICE = ("Mobster is replaying this check's recorded steps with no model. Recorded step {n} was: {action}. On this "
|
|
33
|
+
"screen {why}. Do what that step was for on the screen you see, then carry on with the check. The replay "
|
|
34
|
+
"takes over again as soon as the screen matches a later recorded step.")
|
|
35
|
+
LIVE_NOTICE = ("Mobster is replaying this check's recorded steps with no model. Recorded step {n} ({action}) is one "
|
|
36
|
+
"you do each run: the text it types isn't in the check's steps. Do it now, then carry on with the "
|
|
37
|
+
"check. The replay takes over again as soon as the screen matches the next recorded step.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def identity(check, binding=None, asset_proof=None):
|
|
41
|
+
"""What a check's path depends on: its steps, its app and how it starts; never its expectations."""
|
|
42
|
+
out = {"kind": "check", "steps": list(check.steps), "bundle": check.bundle_id,
|
|
43
|
+
"app": (check.app_path or "").rsplit("/", 1)[-1] or None, "args": list(check.launch_args),
|
|
44
|
+
"env": dict(sorted(check.launch_env.items())), "url": check.open_url, "reset": check.reset}
|
|
45
|
+
business = getattr(check, "business", None)
|
|
46
|
+
if business is not None:
|
|
47
|
+
if binding is None:
|
|
48
|
+
from ..verify.business import load_business_binding
|
|
49
|
+
from ..verify.checks import project_root
|
|
50
|
+
binding = load_business_binding(check, project_root(check.source))
|
|
51
|
+
out["business"] = {"adapter": business.adapter, "version": "mobster.business/1",
|
|
52
|
+
"fixture": business.fixture, "seedSha256": binding.action_digest}
|
|
53
|
+
if getattr(check, "conditions", None):
|
|
54
|
+
out["conditions"] = check.conditions
|
|
55
|
+
seed = getattr(check, "seed", None)
|
|
56
|
+
if getattr(check, "fixture", ()):
|
|
57
|
+
out["fixture"] = {"names": list(check.fixture), "keychain": getattr(seed, "keychain", False),
|
|
58
|
+
"pasteboard": getattr(seed, "pasteboard", "")}
|
|
59
|
+
if asset_proof is not None and (getattr(check, "fixture", ()) or getattr(check, "conditions", None)):
|
|
60
|
+
# Assertion baselines and expected business values do not change the action path.
|
|
61
|
+
out["evidence"] = {name: asset_proof[name] for name in ("seedSha256", "mediaSha256", "pushSha256")}
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _source_hash(source):
|
|
66
|
+
try:
|
|
67
|
+
return hashlib.sha256(Path(source).read_bytes()).hexdigest() if source else None
|
|
68
|
+
except OSError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _proof(check):
|
|
73
|
+
"""Freeze the effective check, including fixture-flattened steps and the current check's assertions."""
|
|
74
|
+
from dataclasses import replace
|
|
75
|
+
|
|
76
|
+
from ..verify.checks import check_to_dict
|
|
77
|
+
seed = getattr(check, "seed", None)
|
|
78
|
+
return {"check": check_to_dict(replace(check, fixture_origin=None)),
|
|
79
|
+
"seed": {"files": [(str(source), destination) for source, destination in
|
|
80
|
+
getattr(seed, "files", ())],
|
|
81
|
+
"keychain": getattr(seed, "keychain", False),
|
|
82
|
+
"pasteboard": getattr(seed, "pasteboard", ""),
|
|
83
|
+
"risky": list(getattr(seed, "risky", ()))},
|
|
84
|
+
"l10n": getattr(check, "l10n", False), "a11yScope": getattr(check, "a11y_scope", "")}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _saved_check(source):
|
|
88
|
+
from dataclasses import replace
|
|
89
|
+
|
|
90
|
+
from ..devkit import links
|
|
91
|
+
from ..verify.checks import CheckError, load_check, project_root
|
|
92
|
+
from ..verify.fixtures import FixtureError, with_fixtures
|
|
93
|
+
root = project_root(source)
|
|
94
|
+
try:
|
|
95
|
+
check = load_check(source)
|
|
96
|
+
if any(links.OPEN_STEP.match(step) for step in check.steps):
|
|
97
|
+
check = replace(check, steps=links.expand_steps(check.steps, links.discover(root, deep=False)))
|
|
98
|
+
return with_fixtures(check, root)
|
|
99
|
+
except (CheckError, FixtureError, links.LinksError) as error:
|
|
100
|
+
raise ValueError(str(error)) from None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _same_assertions(actual, saved):
|
|
104
|
+
"""Planning may add audits, but cannot remove an assertion from the saved check."""
|
|
105
|
+
return (actual.expect == saved.expect and actual.throughout == saved.throughout
|
|
106
|
+
and actual.somewhere == saved.somewhere and actual.business == saved.business
|
|
107
|
+
and set(saved.audit).issubset(actual.audit)
|
|
108
|
+
and (saved.a11y is None or actual.a11y == saved.a11y or
|
|
109
|
+
(saved.a11y == "warn" and actual.a11y == "strict")))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cache_dir_for(runs_dir):
|
|
113
|
+
"""`.mobster/cache/replay` beside `.mobster/runs` (runs_dir's sibling ``cache/replay``)."""
|
|
114
|
+
from pathlib import Path
|
|
115
|
+
return Path(runs_dir).resolve().parent / "cache" / "replay"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def text_sources(check):
|
|
119
|
+
return list(check.steps) + list(check.launch_args) + list(check.launch_env.values()) + [check.open_url or ""]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def history_line(number, step):
|
|
123
|
+
target = step.get("target") or {}
|
|
124
|
+
what = target.get("label") or step.get("app") or ""
|
|
125
|
+
return f"{number}.0: {step.get('op')} {what[:60]!r}" + (" text=…" if step.get("text") or step.get("param")
|
|
126
|
+
else "") + " -> screen changed (replayed)"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class CheckReplay:
|
|
130
|
+
"""Replay state for one VerifyRun."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, run, mode):
|
|
133
|
+
if mode not in MODES:
|
|
134
|
+
raise ValueError(f"replay must be one of {', '.join(MODES)}")
|
|
135
|
+
self.run, self.mode = run, mode
|
|
136
|
+
self.cache = Cache(cache_dir_for(run.runs_dir))
|
|
137
|
+
self.identity = None
|
|
138
|
+
self.path = None
|
|
139
|
+
self.reason = None # why this run didn't replay, in a sentence
|
|
140
|
+
self.result = None
|
|
141
|
+
self.guard = None
|
|
142
|
+
self._recorder = None
|
|
143
|
+
self.heal_cost = 0.0
|
|
144
|
+
self.heal_file = None
|
|
145
|
+
self._looked = False
|
|
146
|
+
self.base_revision = ""
|
|
147
|
+
self.pin = None
|
|
148
|
+
self._pin_error = None
|
|
149
|
+
self._pin_refused = False
|
|
150
|
+
self._pending_heals = 0
|
|
151
|
+
self._legacy_pending = False
|
|
152
|
+
self._source_hash = None
|
|
153
|
+
self._proof = None
|
|
154
|
+
self._run_proof = None
|
|
155
|
+
self._asset_proof = None
|
|
156
|
+
|
|
157
|
+
def env(self):
|
|
158
|
+
from ..verify.smart import is_real_device
|
|
159
|
+
run = self.run
|
|
160
|
+
real = is_real_device(run.manager)
|
|
161
|
+
model = getattr(run.target, "device_type", None)
|
|
162
|
+
if real and model == "iPhone":
|
|
163
|
+
model = None # WDA's fallback is not a physical model identity.
|
|
164
|
+
return env_of(getattr(run.app, "version", None), getattr(run.target, "runtime", None),
|
|
165
|
+
"iphone" if real else "simulator", device_type=model)
|
|
166
|
+
|
|
167
|
+
def refuse_pin(self, reason):
|
|
168
|
+
"""Report pre-action repeat refusal even when asset admission precedes cache lookup."""
|
|
169
|
+
self._looked = True
|
|
170
|
+
self._pin_refused = True
|
|
171
|
+
self._pin_error = self.reason = reason
|
|
172
|
+
from ..verify.runner import CouldntRun
|
|
173
|
+
raise CouldntRun("replay", reason)
|
|
174
|
+
|
|
175
|
+
def lookup(self):
|
|
176
|
+
"""The cached path to replay, or None (``reason`` says why not). Looked up once per run."""
|
|
177
|
+
if self._looked:
|
|
178
|
+
if self._pin_error:
|
|
179
|
+
from ..verify.runner import CouldntRun
|
|
180
|
+
raise CouldntRun("replay", self._pin_error)
|
|
181
|
+
return self.path
|
|
182
|
+
if self.mode == "off":
|
|
183
|
+
return None
|
|
184
|
+
binding = getattr(self.run, "business_binding", None)
|
|
185
|
+
if getattr(self.run.check, "business", None) is not None:
|
|
186
|
+
from ..verify.business import load_business_binding
|
|
187
|
+
from ..verify.checks import CheckError, project_root
|
|
188
|
+
from ..verify.runner import CouldntRun
|
|
189
|
+
if not self.run.check.source:
|
|
190
|
+
raise CouldntRun("business", "An app-state check must be a saved project check file.")
|
|
191
|
+
try:
|
|
192
|
+
current = load_business_binding(self.run.check, project_root(self.run.check.source))
|
|
193
|
+
except CheckError as error:
|
|
194
|
+
raise CouldntRun("business", str(error),
|
|
195
|
+
"Fix the enrolled app-state fixture, then run again.") from None
|
|
196
|
+
if binding is not None and binding.digest != current.digest:
|
|
197
|
+
raise CouldntRun("business", "The enrolled app-state fixture changed before replay admission.")
|
|
198
|
+
binding = self.run.business_binding = current
|
|
199
|
+
self._source_hash = _source_hash(self.run.check.source)
|
|
200
|
+
self._run_proof = _proof(self.run.check)
|
|
201
|
+
if self.run.check.source:
|
|
202
|
+
try:
|
|
203
|
+
saved = _saved_check(self.run.check.source)
|
|
204
|
+
if not _same_assertions(self.run.check, saved):
|
|
205
|
+
raise ValueError("the saved check's assertions changed after admission")
|
|
206
|
+
self._proof = _proof(saved)
|
|
207
|
+
except (OSError, ValueError) as error:
|
|
208
|
+
self._looked = True
|
|
209
|
+
self.reason = str(error)
|
|
210
|
+
if self.mode == "strict" or self.pin is not None:
|
|
211
|
+
self.refuse_pin(self.reason)
|
|
212
|
+
return None
|
|
213
|
+
else:
|
|
214
|
+
self._proof = self._run_proof
|
|
215
|
+
from . import heal as heals
|
|
216
|
+
try:
|
|
217
|
+
self._asset_proof = heals.proof_assets(self.run.check)
|
|
218
|
+
except heals.HealError as error:
|
|
219
|
+
self._looked = True
|
|
220
|
+
self.reason = f"the check's evidence could not be frozen: {error}"
|
|
221
|
+
if self.mode == "strict" or self.pin is not None:
|
|
222
|
+
self.refuse_pin(self.reason)
|
|
223
|
+
return None
|
|
224
|
+
self.identity = identity(self.run.check, binding, self._asset_proof)
|
|
225
|
+
self._looked = True
|
|
226
|
+
env = self.env()
|
|
227
|
+
path = self.cache.load(self.identity, env)
|
|
228
|
+
self._legacy_pending = bool(path and path.get("pending"))
|
|
229
|
+
self.base_revision = revision(path) if path else ""
|
|
230
|
+
if self.pin is not None:
|
|
231
|
+
pin_env = {**env, "runtime": getattr(self.run.target, "runtime", None)}
|
|
232
|
+
self._pin_error = self.pin.bind(self.identity, pin_env,
|
|
233
|
+
"" if self._legacy_pending else self.base_revision)
|
|
234
|
+
if self._pin_error:
|
|
235
|
+
self.refuse_pin(self._pin_error)
|
|
236
|
+
source = self.run.check.source
|
|
237
|
+
if source:
|
|
238
|
+
from . import heal as heals
|
|
239
|
+
try:
|
|
240
|
+
document = heals.read(source)
|
|
241
|
+
self._pending_heals = len(document["heals"]) if document is not None else 0
|
|
242
|
+
except heals.HealError:
|
|
243
|
+
self._pending_heals = 1
|
|
244
|
+
if self.mode == "fresh":
|
|
245
|
+
self.reason = "--fresh"
|
|
246
|
+
return None
|
|
247
|
+
if self._legacy_pending:
|
|
248
|
+
self.reason = "an older unreviewed heal is in the cache; record again with --fresh"
|
|
249
|
+
return None
|
|
250
|
+
if env["model"] is None:
|
|
251
|
+
self.reason = "the device model is unknown"
|
|
252
|
+
return None
|
|
253
|
+
if path is None:
|
|
254
|
+
others = self.cache.others(self.identity)
|
|
255
|
+
if others:
|
|
256
|
+
was = others[0]
|
|
257
|
+
if was.get("app_version") != env["app_version"]:
|
|
258
|
+
self.reason = (f"the app changed (recorded on {was.get('app_version') or 'an unknown version'}, "
|
|
259
|
+
f"this is {env['app_version'] or 'an unknown version'})")
|
|
260
|
+
elif was.get("ios") != env["ios"]:
|
|
261
|
+
self.reason = (f"iOS changed (recorded on iOS {was.get('ios') or '?'}, this is iOS "
|
|
262
|
+
f"{env['ios'] or '?'})")
|
|
263
|
+
elif was.get("device") != env["device"]:
|
|
264
|
+
self.reason = "it was recorded on another kind of device"
|
|
265
|
+
elif was.get("model") != env["model"]:
|
|
266
|
+
self.reason = (f"the device model changed (recorded on {was.get('model') or 'an unknown model'}, "
|
|
267
|
+
f"this is {env['model'] or 'an unknown model'})")
|
|
268
|
+
else:
|
|
269
|
+
self.reason = "no matching recorded run"
|
|
270
|
+
else:
|
|
271
|
+
self.reason = "no recorded run yet"
|
|
272
|
+
return None
|
|
273
|
+
if self.mode == "strict" or self.pin is not None:
|
|
274
|
+
live = next(((index, step) for index, step in enumerate(path["steps"], 1) if step.get("live")), None)
|
|
275
|
+
if live is not None:
|
|
276
|
+
index, step = live
|
|
277
|
+
self.reason = f"recorded step {index} ({describe(step)}) needs Smart"
|
|
278
|
+
if self.pin is not None:
|
|
279
|
+
self.refuse_pin(f"A fixed replay cannot run: {self.reason}.")
|
|
280
|
+
return None
|
|
281
|
+
if not replayable_count(path):
|
|
282
|
+
self.reason = "every recorded step needs the model"
|
|
283
|
+
return None
|
|
284
|
+
self.path = path
|
|
285
|
+
return path
|
|
286
|
+
|
|
287
|
+
def recorder(self):
|
|
288
|
+
if not self._looked:
|
|
289
|
+
self.lookup() # capture the approved base before Smart spends time acting
|
|
290
|
+
self._recorder = Recorder(text_policy=literal_text(text_sources(self.run.check)))
|
|
291
|
+
return self._recorder
|
|
292
|
+
|
|
293
|
+
def replay(self, cancelled=None):
|
|
294
|
+
"""Replay the cached path on the run's driver. Returns engine.Result."""
|
|
295
|
+
run = self.run
|
|
296
|
+
from .. import engines
|
|
297
|
+
from ..verify.smart import check_approver, check_guard
|
|
298
|
+
# Without a key nothing can heal: a changed step fails the check, as with --no-heal.
|
|
299
|
+
self.can_heal = self.mode != "strict" and self.pin is None and not self._pending_heals \
|
|
300
|
+
and bool(run.key or engines.smart_key())
|
|
301
|
+
heal = self._heal if self.can_heal else None
|
|
302
|
+
guard = self.guard = check_guard(run.manager, run.app.bundle_id)
|
|
303
|
+
names = {run.app.bundle_id: run.app.name}
|
|
304
|
+
def emit(event):
|
|
305
|
+
run.event(event)
|
|
306
|
+
if event.get("event") == "replay_step":
|
|
307
|
+
run.record_step(op=event["op"], text=f"Replayed: {event['action']}",
|
|
308
|
+
target={"id": None, "label": event.get("label"), "role": event.get("role")},
|
|
309
|
+
typed=event.get("typed"), changed=True)
|
|
310
|
+
elif event.get("event") == "replay_drift":
|
|
311
|
+
run.progress(f" Step {event['step']} changed: {event['why']}")
|
|
312
|
+
|
|
313
|
+
replayer = Replayer(run.driver, approve=check_approver(run.manager), heal=heal,
|
|
314
|
+
live=None if self.mode == "strict" or self.pin is not None else self._live,
|
|
315
|
+
emit=emit, cancelled=lambda: run._stop.is_set() or bool(cancelled and cancelled()),
|
|
316
|
+
frame=lambda label: run.frame(label), on_commit="deny", app_names=names,
|
|
317
|
+
allows_app=guard.allows_app if guard is not None else None,
|
|
318
|
+
dialogs=self._responder(emit))
|
|
319
|
+
self._steps = self.path["steps"]
|
|
320
|
+
self._healed_at = {}
|
|
321
|
+
try:
|
|
322
|
+
result = replayer.run(self.path)
|
|
323
|
+
finally:
|
|
324
|
+
if guard is not None and guard.left:
|
|
325
|
+
run._left_app = guard.left
|
|
326
|
+
if result.status == "drift" and self._pending_heals:
|
|
327
|
+
result.reason += " Review the pending heal with mobster test heal before healing another change."
|
|
328
|
+
if guard is not None:
|
|
329
|
+
# The final action can itself open another app. Check that screen too, before VerifyRun reads its
|
|
330
|
+
# evidence or captures a verdict frame (there may be no next replay step to check it).
|
|
331
|
+
bundle = getattr(result.snapshot, "bundle_id", None)
|
|
332
|
+
if bundle and bundle != "com.apple.springboard":
|
|
333
|
+
guard.allows_app(bundle)
|
|
334
|
+
run._left_app = guard.left
|
|
335
|
+
self.result = result
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
def _responder(self, emit):
|
|
339
|
+
"""Answers the dialogs a replay meets from the check's ``dialogs``; None when the run has no policy."""
|
|
340
|
+
run = self.run
|
|
341
|
+
if getattr(run, "dialogs", None) is None:
|
|
342
|
+
return None
|
|
343
|
+
from ..verify.dialogs import Responder
|
|
344
|
+
return Responder(run.dialogs, run.driver, emit=emit, log=run.log)
|
|
345
|
+
|
|
346
|
+
def _heal(self, index, step, snapshot, why, done_steps, limit=None):
|
|
347
|
+
self._healed_at[index] = self._healed_at.get(index, 0) + 1
|
|
348
|
+
if self._healed_at[index] > HEALS_PER_STEP:
|
|
349
|
+
return Heal(False, reason=f"step {index + 1} changed again after it healed")
|
|
350
|
+
feedback = NOTICE.format(n=index + 1, action=describe(step), why=why)
|
|
351
|
+
return self._smart(index, done_steps, feedback, start=index, limit=limit)
|
|
352
|
+
|
|
353
|
+
def _live(self, index, step, snapshot, done_steps):
|
|
354
|
+
feedback = LIVE_NOTICE.format(n=index + 1, action=describe(step))
|
|
355
|
+
return self._smart(index, done_steps, feedback, start=index + 1)
|
|
356
|
+
|
|
357
|
+
def _smart(self, index, done_steps, feedback, *, start, limit=None):
|
|
358
|
+
from .. import engines, harness_api, smart_events
|
|
359
|
+
from ..model_clients import cost_usd
|
|
360
|
+
from ..verify.smart import check_approver, smart_request
|
|
361
|
+
run = self.run
|
|
362
|
+
steps = self._steps
|
|
363
|
+
key = run.key or engines.smart_key()
|
|
364
|
+
if not key:
|
|
365
|
+
return Heal(False, reason="Smart has no key, so the step couldn't heal")
|
|
366
|
+
spent = run.cost_usd + self.heal_cost
|
|
367
|
+
budget = float(run.check.max_usd) - spent
|
|
368
|
+
if budget <= 0:
|
|
369
|
+
return Heal(False, reason=f"the check's ${run.check.max_usd:.2f} spend cap was reached", status="budget")
|
|
370
|
+
recorder = Recorder(text_policy=literal_text(text_sources(run.check)))
|
|
371
|
+
rejoin = {}
|
|
372
|
+
candidates = [j for j in range(start, min(len(steps), start + LOOKAHEAD + 1))
|
|
373
|
+
if not steps[j].get("live") and steps[j].get("op") not in ("LAUNCH_APP", "HOME", "WAIT")]
|
|
374
|
+
|
|
375
|
+
def observer(record):
|
|
376
|
+
recorder.observe_step(record)
|
|
377
|
+
screen = recorder.latest
|
|
378
|
+
if screen is None or "at" in rejoin or "breaker" in rejoin:
|
|
379
|
+
return
|
|
380
|
+
for j in candidates:
|
|
381
|
+
ok, _, _ = match(steps[j], screen)
|
|
382
|
+
if ok and screen_likeness(steps[j], screen) >= STRONG:
|
|
383
|
+
rejoin["at"] = j
|
|
384
|
+
return
|
|
385
|
+
if limit is not None and len(recorder.steps) >= limit:
|
|
386
|
+
rejoin["breaker"] = True # the next action would heal more than half the path
|
|
387
|
+
|
|
388
|
+
def stopping():
|
|
389
|
+
return "at" in rejoin or "breaker" in rejoin or run._stop.is_set()
|
|
390
|
+
|
|
391
|
+
client = engines.build_client(key=key)
|
|
392
|
+
events = []
|
|
393
|
+
unknown = []
|
|
394
|
+
|
|
395
|
+
def on_event(event):
|
|
396
|
+
run.event({**event, "heal": index + 1} if isinstance(event, dict) else event)
|
|
397
|
+
if (isinstance(event, dict) and event.get("event") == "frontier_failed"
|
|
398
|
+
and event.get("outcome") == "unknown"):
|
|
399
|
+
unknown.append(event) # an action that may have landed (frontier/act.py:_execute)
|
|
400
|
+
if isinstance(event, dict) and event.get("event") == "frontier_action":
|
|
401
|
+
events.append(event)
|
|
402
|
+
op, label = event.get("operation") or "", event.get("target_label") or ""
|
|
403
|
+
run.record_step(op=op,
|
|
404
|
+
text=f"Healed: {smart_events.step_text(op, label, {run.app.bundle_id: run.app.name},
|
|
405
|
+
text=event.get('text'))}",
|
|
406
|
+
target={"id": None, "label": label or None, "role": None},
|
|
407
|
+
typed=event.get("text") if op in smart_events.TYPING else None,
|
|
408
|
+
changed=event.get("changed"))
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
agent = engines.build_frontier(recorder.wrap(run.driver), client, apps={run.app.bundle_id: run.app.name},
|
|
412
|
+
emit=on_event, max_cost_usd=budget, approve=check_approver(run.manager),
|
|
413
|
+
cancelled=stopping, guard=self.guard, dialogs=True,
|
|
414
|
+
dialog_policy=getattr(run, "dialogs", None),
|
|
415
|
+
step_observer=observer, max_steps=HEAL_TURNS,
|
|
416
|
+
max_seconds=max(20.0, float(run.check.max_seconds) - (time.monotonic()
|
|
417
|
+
- run._t0)))
|
|
418
|
+
initial = harness_api.InitialState(
|
|
419
|
+
history=tuple(history_line(n, s) for n, s in enumerate(done_steps, 1))[-20:], feedback=feedback,
|
|
420
|
+
reason="a replayed check step changed on screen")
|
|
421
|
+
outcome = agent.run(smart_request(run.app.name, run.check.steps), initial=initial)
|
|
422
|
+
finally:
|
|
423
|
+
if self.guard is not None and self.guard.left:
|
|
424
|
+
run._left_app = self.guard.left
|
|
425
|
+
engines.release_client(client)
|
|
426
|
+
cost = cost_usd(getattr(client, "model", ""), outcome.get("usage") or getattr(client, "usage", {}) or {}) or 0
|
|
427
|
+
self.heal_cost += cost
|
|
428
|
+
status = outcome.get("status")
|
|
429
|
+
if unknown and status != "done" and "at" not in rejoin:
|
|
430
|
+
# An action's outcome is unknown and the heal never settled it: stop, so no retry sends it again.
|
|
431
|
+
reason = str(outcome.get("reason") or outcome.get("answer") or status or "")[:300]
|
|
432
|
+
return Heal(False, steps=list(recorder.steps), cost_usd=cost, status="unknown_outcome", reason=reason)
|
|
433
|
+
if "breaker" in rejoin:
|
|
434
|
+
return Heal(False, steps=list(recorder.steps), cost_usd=cost, status="breaker",
|
|
435
|
+
reason="more than half the recorded steps would heal")
|
|
436
|
+
if "at" in rejoin:
|
|
437
|
+
return Heal(True, steps=list(recorder.steps), resume_at=rejoin["at"], cost_usd=cost)
|
|
438
|
+
if status == "done":
|
|
439
|
+
return Heal(True, steps=list(recorder.steps), done=True, cost_usd=cost, status=status)
|
|
440
|
+
reason = str(outcome.get("reason") or outcome.get("answer") or status or "")[:300]
|
|
441
|
+
return Heal(False, steps=list(recorder.steps), cost_usd=cost, status=status,
|
|
442
|
+
reason=f"Smart couldn't heal step {index + 1} ({status}{': ' + reason if reason else ''})")
|
|
443
|
+
|
|
444
|
+
# -- after the verdict -------------------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
def summary(self):
|
|
447
|
+
"""results.json's ``replay`` for this run."""
|
|
448
|
+
result = self.result
|
|
449
|
+
recorded = self._recorder.steps if self._recorder is not None else []
|
|
450
|
+
approved = self.base_revision if self.path is not None else None
|
|
451
|
+
fixed = bool(self.pin is not None and approved and self.mode != "fresh")
|
|
452
|
+
if result is None:
|
|
453
|
+
if (self.mode == "strict" or self._pin_refused) and self._looked and self.path is None:
|
|
454
|
+
return {"mode": "unavailable", "status": "unavailable", "reason": self.reason, "steps": 0,
|
|
455
|
+
"replayed": 0, "healed": 0, "live": 0, "costUsd": 0, "saved": False, "cache": None,
|
|
456
|
+
"pendingHeals": 0, "approvedRevision": None, "fixed": False}
|
|
457
|
+
return {"mode": "live", "reason": self.reason, "steps": len(recorded), "replayed": 0, "healed": 0,
|
|
458
|
+
"live": len(recorded), "costUsd": None, "saved": False, "cache": None,
|
|
459
|
+
"pendingHeals": 0, "approvedRevision": None, "fixed": False}
|
|
460
|
+
mode = "healed" if result.heals or result.live else "replayed"
|
|
461
|
+
return {"mode": mode, "status": result.status, "reason": result.reason or None,
|
|
462
|
+
"steps": len(self.path["steps"]), "replayed": result.replayed, "healed": len(result.heals),
|
|
463
|
+
"live": result.live, "seconds": result.seconds, "costUsd": round(result.cost_usd, 5),
|
|
464
|
+
"saved": False, "cache": None, "pendingHeals": self._pending_heals,
|
|
465
|
+
"approvedRevision": approved, "fixed": fixed}
|
|
466
|
+
|
|
467
|
+
def after(self, result):
|
|
468
|
+
"""Add ``replay`` and ``healed`` to the run's ``result``, and keep what a passing run proved."""
|
|
469
|
+
info = self.summary()
|
|
470
|
+
healed = [dict(heal) for heal in (self.result.heals if self.result is not None else ())]
|
|
471
|
+
passed = result.get("verdict") == "passed"
|
|
472
|
+
refreshing_a11y = bool(getattr(self.run.check, "a11y_refresh", False))
|
|
473
|
+
# A trial with the saved check's steps and app name shares its cache file, so saving replaces the approved path.
|
|
474
|
+
draft_trial = bool(getattr(self.run.check, "draft_trial", False))
|
|
475
|
+
# A new heal cannot become an approved path during baseline replacement, even when the only failed
|
|
476
|
+
# assertion is a new accessibility finding (which baseline refresh otherwise accepts).
|
|
477
|
+
unstaged = bool(refreshing_a11y and self.result is not None and self.result.status == "done" and healed
|
|
478
|
+
and result.get("verdict") in ("passed", "failed"))
|
|
479
|
+
try:
|
|
480
|
+
if self.result is None:
|
|
481
|
+
if passed and not refreshing_a11y and not draft_trial and self.mode != "off" \
|
|
482
|
+
and self._recorder is not None and self._recorder.steps:
|
|
483
|
+
info.update(self._save_new())
|
|
484
|
+
elif self.result.status == "done" and passed:
|
|
485
|
+
if not refreshing_a11y:
|
|
486
|
+
info.update(self._save_replayed(healed))
|
|
487
|
+
unstaged = bool(healed and not info.get("pendingHeals"))
|
|
488
|
+
# A failed or uncertain action never revokes the approved path.
|
|
489
|
+
except Exception as error:
|
|
490
|
+
self.run.log(f"replay cache not written: {type(error).__name__}: {error}")
|
|
491
|
+
unstaged = unstaged or bool(passed and healed)
|
|
492
|
+
if unstaged:
|
|
493
|
+
from ..verify.verdict import EXIT_CODES
|
|
494
|
+
message = ("The check healed during accessibility baseline refresh; its approved path and baseline were "
|
|
495
|
+
"kept." if refreshing_a11y else
|
|
496
|
+
"The check's assertions held, but its healed path could not be staged for review.")
|
|
497
|
+
fix = ("Run mobster test without --update-a11y-baseline to review the heal, then refresh the baseline "
|
|
498
|
+
"in a separate run." if refreshing_a11y else
|
|
499
|
+
"Inspect the check and replay cache, then run mobster test --fresh.")
|
|
500
|
+
info.update(status="proposal_unavailable", reason=message, saved=False, pendingHeals=0)
|
|
501
|
+
result.update(verdict="couldnt_run", exit_code=EXIT_CODES["couldnt_run"], summary=message,
|
|
502
|
+
reason={"class": "replay", "message": message, "fix": fix})
|
|
503
|
+
result["replay"] = info
|
|
504
|
+
result["healed"] = healed
|
|
505
|
+
if self.heal_file is not None:
|
|
506
|
+
result["healDiff"] = str(self.heal_file)
|
|
507
|
+
return result
|
|
508
|
+
|
|
509
|
+
def _save_new(self):
|
|
510
|
+
if self.pin is not None:
|
|
511
|
+
return {} # Repetitions observe one frozen baseline; they do not publish a new one.
|
|
512
|
+
if not self._looked:
|
|
513
|
+
self.lookup()
|
|
514
|
+
if self.identity is None or self._asset_proof is None:
|
|
515
|
+
self.run.log("the check's evidence was not frozen; the replay path was not saved")
|
|
516
|
+
return {}
|
|
517
|
+
steps = self._recorder.steps
|
|
518
|
+
env = self.env()
|
|
519
|
+
if env["model"] is None:
|
|
520
|
+
return {} # Without a model this recording cannot safely replay on the next acquired device.
|
|
521
|
+
path = new_path(steps, kind="check", identity=self.identity, env=env, source_run=self.run.run_id)
|
|
522
|
+
with self.cache.locked():
|
|
523
|
+
current = self.cache.load(self.identity, env)
|
|
524
|
+
if (revision(current) if current else "") != self.base_revision:
|
|
525
|
+
self.run.log("replay cache changed during Smart; the new path was not saved")
|
|
526
|
+
return {}
|
|
527
|
+
if self._legacy_pending and self.mode != "fresh":
|
|
528
|
+
self.run.log("an older unreviewed heal remains; use --fresh to replace its path")
|
|
529
|
+
return {}
|
|
530
|
+
file = self.cache.save(path)
|
|
531
|
+
self._drop_superseded_heals(file.name)
|
|
532
|
+
return {"saved": True, "cache": file.name, "steps": len(steps)}
|
|
533
|
+
|
|
534
|
+
def _drop_superseded_heals(self, cache_name):
|
|
535
|
+
"""A new recording replaces the steps a waiting heal diff described: that diff goes with them."""
|
|
536
|
+
source = self.run.check.source
|
|
537
|
+
if not source:
|
|
538
|
+
return
|
|
539
|
+
from . import heal as heals
|
|
540
|
+
if heals.discard(source, self.cache.directory, cache_name):
|
|
541
|
+
self.run.log(f"{heals.heal_file(source).name} described steps recorded again; removed it")
|
|
542
|
+
|
|
543
|
+
def _save_replayed(self, healed):
|
|
544
|
+
if not healed or self.pin is not None:
|
|
545
|
+
return {}
|
|
546
|
+
old = self.path
|
|
547
|
+
path = dict(old)
|
|
548
|
+
path["steps"] = list(self.result.steps)
|
|
549
|
+
path["strict"] = list(self.result.strict)
|
|
550
|
+
source = self.run.check.source
|
|
551
|
+
if not source:
|
|
552
|
+
self.run.log("the healed path has no check file for review; approved steps were kept")
|
|
553
|
+
return {}
|
|
554
|
+
try:
|
|
555
|
+
current_check = _saved_check(source)
|
|
556
|
+
except (OSError, ValueError):
|
|
557
|
+
current_check = None
|
|
558
|
+
if (_source_hash(source) != self._source_hash or current_check is None
|
|
559
|
+
or _proof(self.run.check) != self._run_proof
|
|
560
|
+
or _proof(current_check) != self._proof):
|
|
561
|
+
self.run.log("the check changed during healing; the approved path was kept without staging a proposal")
|
|
562
|
+
return {}
|
|
563
|
+
from . import heal as heals
|
|
564
|
+
proof = self._asset_proof
|
|
565
|
+
if proof is None:
|
|
566
|
+
self.run.log("the check's evidence was not frozen before replay; proposal was not staged")
|
|
567
|
+
return {}
|
|
568
|
+
try:
|
|
569
|
+
current_proof = heals.proof_assets(current_check, include_a11y=proof["a11ySha256"] is not None)
|
|
570
|
+
except HealError:
|
|
571
|
+
current_proof = None
|
|
572
|
+
if current_proof != proof:
|
|
573
|
+
self.run.log("the check's evidence changed during healing; proposal was not staged")
|
|
574
|
+
return {}
|
|
575
|
+
with self.cache.locked():
|
|
576
|
+
current = self.cache.load(self.identity, self.env())
|
|
577
|
+
if current is None or revision(current) != self.base_revision:
|
|
578
|
+
self.run.log("approved replay changed during healing; proposal was not staged")
|
|
579
|
+
return {}
|
|
580
|
+
app = self.run.app
|
|
581
|
+
self.heal_file = heals.record(source, healed, run_id=self.run.run_id,
|
|
582
|
+
cache_file=self.cache._file(self.identity, self.env()),
|
|
583
|
+
candidate=path, base_revision=self.base_revision,
|
|
584
|
+
check_sha256=self._source_hash,
|
|
585
|
+
effective_sha256=heals.effective_digest(current_check), proof=proof,
|
|
586
|
+
app={"bundle": app.bundle_id, "version": app.version or None},
|
|
587
|
+
ios=self.env()["ios"], device=self.env()["device"])
|
|
588
|
+
for item in healed:
|
|
589
|
+
item["diff"] = str(self.heal_file)
|
|
590
|
+
return {"saved": False, "cache": self.cache._file(self.identity, self.env()).name,
|
|
591
|
+
"pendingHeals": len(healed)}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
__all__ = ["CheckReplay", "identity", "cache_dir_for", "MODES"]
|