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,364 @@
|
|
|
1
|
+
"""Heals as reviewable diffs: ``<check>.heal.yaml`` next to the check, Accept and Keep failing.
|
|
2
|
+
|
|
3
|
+
A heal is written the moment a run heals, never applied silently:
|
|
4
|
+
|
|
5
|
+
- the run's results.json lists it in ``healed[]`` (step, from, to, why, frames before and after);
|
|
6
|
+
- ``<check>.heal.yaml`` holds every heal not yet reviewed, beside the check, so it shows in a pull request;
|
|
7
|
+
- the approved cached path stays unchanged until Accept; the full proposal stays private in the replay cache.
|
|
8
|
+
|
|
9
|
+
Review:
|
|
10
|
+
|
|
11
|
+
- **Accept** (``mobster test heal --accept``): the proposed steps become approved; the .heal.yaml file goes.
|
|
12
|
+
- **Keep failing** (``--reject``): the approved steps stay and become strict at the changed positions: when the
|
|
13
|
+
screen differs there again, the check fails instead of healing. Use it when the change is a bug.
|
|
14
|
+
|
|
15
|
+
A heal never touches the check file: its steps and its expectations stay as you wrote them.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import stat
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from .path import PathError, validate
|
|
27
|
+
from .store import NAME, Cache, atomic_write, file_name, revision, sync_directory
|
|
28
|
+
|
|
29
|
+
VERSION = 2
|
|
30
|
+
SUFFIX = ".heal.yaml"
|
|
31
|
+
PROPOSAL = re.compile(r"[0-9a-f]{64}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HealError(Exception):
|
|
35
|
+
"""A heal file or its cached path that can't be reviewed: one plain sentence."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InputChanged(HealError):
|
|
39
|
+
"""A check's input differs from what the check was admitted with, as opposed to one that can't be read."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def heal_file(check_file):
|
|
43
|
+
"""``<checks>/paywall.heal.yaml`` for ``<checks>/paywall.yaml``."""
|
|
44
|
+
check_file = Path(check_file)
|
|
45
|
+
return check_file.with_name(check_file.name.rsplit(".", 1)[0] + SUFFIX)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _header(check_name):
|
|
49
|
+
return ("# Mobster heal diff: steps of this check changed on screen and Smart healed them. Review them.\n"
|
|
50
|
+
"# A heal never changes an expectation. The check file itself is untouched.\n"
|
|
51
|
+
f"# Accept: mobster test heal --accept {check_name}\n"
|
|
52
|
+
f"# Keep failing: mobster test heal --reject {check_name}\n")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read(check_file):
|
|
56
|
+
"""The heal document for ``check_file``, or None when nothing waits for review. Raises HealError when the file
|
|
57
|
+
exists but can't be read."""
|
|
58
|
+
path = heal_file(check_file)
|
|
59
|
+
if not path.is_file():
|
|
60
|
+
return None
|
|
61
|
+
import yaml
|
|
62
|
+
try:
|
|
63
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
64
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError) as error:
|
|
65
|
+
raise HealError(f"{path.name} can't be read ({type(error).__name__})") from None
|
|
66
|
+
if not isinstance(data, dict) or not isinstance(data.get("heals"), list):
|
|
67
|
+
raise HealError(f"{path.name} isn't a heal diff Mobster wrote")
|
|
68
|
+
return data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _write(path, document):
|
|
72
|
+
import yaml
|
|
73
|
+
text = _header(Path(document.get("check") or path.name).name) + yaml.safe_dump(
|
|
74
|
+
document, sort_keys=False, allow_unicode=True, default_flow_style=False, width=110)
|
|
75
|
+
atomic_write(path, text)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _digest(value):
|
|
79
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False,
|
|
80
|
+
separators=(",", ":")).encode()).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def effective_digest(check):
|
|
84
|
+
"""Bind fixture-flattened execution and assertions without storing their text in the proposal."""
|
|
85
|
+
from .checks import _proof
|
|
86
|
+
return _digest(_proof(check))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _proposal_file(cache_dir, cache_name, proposal):
|
|
90
|
+
if not isinstance(cache_name, str) or not NAME.fullmatch(cache_name) or not isinstance(proposal, str) \
|
|
91
|
+
or not PROPOSAL.fullmatch(proposal):
|
|
92
|
+
raise HealError("the heal file doesn't name a valid recorded proposal")
|
|
93
|
+
return Path(cache_dir) / f"{cache_name[:-5]}-{proposal}.proposal.json"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _check_digest(check_file):
|
|
97
|
+
try:
|
|
98
|
+
return hashlib.sha256(Path(check_file).read_bytes()).hexdigest()
|
|
99
|
+
except OSError as error:
|
|
100
|
+
raise HealError(f"the check file can't be read ({type(error).__name__})") from None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _file_digest(path, limit):
|
|
104
|
+
"""Hash one regular asset without following its final symlink or accepting an in-place rewrite."""
|
|
105
|
+
try:
|
|
106
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
107
|
+
try:
|
|
108
|
+
before = os.fstat(fd)
|
|
109
|
+
if not stat.S_ISREG(before.st_mode) or before.st_size > limit:
|
|
110
|
+
raise HealError("an external check asset is not a bounded regular file")
|
|
111
|
+
digest = hashlib.sha256()
|
|
112
|
+
size = 0
|
|
113
|
+
while chunk := os.read(fd, min(65536, limit + 1 - size)):
|
|
114
|
+
size += len(chunk)
|
|
115
|
+
if size > limit:
|
|
116
|
+
raise HealError("an external check asset exceeds its proof limit")
|
|
117
|
+
digest.update(chunk)
|
|
118
|
+
after = os.fstat(fd)
|
|
119
|
+
identity = lambda item: (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns, item.st_ctime_ns)
|
|
120
|
+
if size != before.st_size or identity(after) != identity(before):
|
|
121
|
+
raise HealError("an external check asset changed while it was read")
|
|
122
|
+
return size, digest.hexdigest()
|
|
123
|
+
finally:
|
|
124
|
+
os.close(fd)
|
|
125
|
+
except OSError as error:
|
|
126
|
+
raise HealError(f"an external check asset can't be read ({type(error).__name__})") from None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _assets(files, *, limit, count_limit):
|
|
130
|
+
rows, total = [], 0
|
|
131
|
+
for label, path in files:
|
|
132
|
+
size, digest = _file_digest(path, limit - total)
|
|
133
|
+
total += size
|
|
134
|
+
rows.append((label, size, digest))
|
|
135
|
+
if len(rows) > count_limit:
|
|
136
|
+
raise HealError("the check has too many external assets to freeze")
|
|
137
|
+
return _digest(rows)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def proof_assets(check, *, include_a11y=False):
|
|
141
|
+
"""Digests of external evidence the parsed check uses to judge a run."""
|
|
142
|
+
from ..verify import conditions, fixtures
|
|
143
|
+
seed = getattr(check, "seed", None)
|
|
144
|
+
seed_files = []
|
|
145
|
+
for source, destination in getattr(seed, "files", ()):
|
|
146
|
+
source = Path(source)
|
|
147
|
+
if source.is_symlink():
|
|
148
|
+
raise HealError("a seed asset is a symlink; the approved path was kept")
|
|
149
|
+
if source.is_file():
|
|
150
|
+
seed_files.append((destination, source))
|
|
151
|
+
elif source.is_dir():
|
|
152
|
+
paths = []
|
|
153
|
+
for path in source.rglob("*"):
|
|
154
|
+
paths.append(path)
|
|
155
|
+
if len(paths) > fixtures.FILES_LIMIT * 8:
|
|
156
|
+
raise HealError("the seed directory has too many entries to freeze")
|
|
157
|
+
for path in sorted(paths):
|
|
158
|
+
if path.is_symlink():
|
|
159
|
+
raise HealError("a seed asset is a symlink; the approved path was kept")
|
|
160
|
+
if path.is_file():
|
|
161
|
+
seed_files.append((str(Path(destination) / path.relative_to(source)), path))
|
|
162
|
+
elif not path.is_dir():
|
|
163
|
+
raise HealError("a seed asset is not a regular file")
|
|
164
|
+
else:
|
|
165
|
+
raise HealError("a seed asset is unavailable; the approved path was kept")
|
|
166
|
+
seed_sha = _assets(seed_files, limit=fixtures.BYTES_LIMIT, count_limit=fixtures.FILES_LIMIT * 4)
|
|
167
|
+
condition = getattr(check, "conditions", None) or {}
|
|
168
|
+
media = condition.get("media") or ()
|
|
169
|
+
media_sha = _assets(((str(index), Path(path)) for index, path in enumerate(media)),
|
|
170
|
+
limit=128 * 1024 * 1024, count_limit=conditions.MEDIA_LIMIT)
|
|
171
|
+
push = condition.get("push")
|
|
172
|
+
push_sha = _file_digest(push, conditions.PAYLOAD_LIMIT)[1] if push else None
|
|
173
|
+
a11y_sha = None
|
|
174
|
+
if include_a11y or getattr(check, "a11y", None) or "a11y" in getattr(check, "audit", ()) \
|
|
175
|
+
or getattr(check, "a11y_refresh", False):
|
|
176
|
+
if getattr(check, "a11y_refresh", False):
|
|
177
|
+
if check.a11y_baseline:
|
|
178
|
+
raise HealError("the accessibility refresh did not apply an empty baseline")
|
|
179
|
+
baseline = check.a11y_baseline # the applied empty set; the old disk baseline is being replaced
|
|
180
|
+
else:
|
|
181
|
+
from ..testkit import a11y_baseline
|
|
182
|
+
from ..verify.checks import project_root
|
|
183
|
+
try:
|
|
184
|
+
baseline = a11y_baseline.load(project_root(check.source))
|
|
185
|
+
except a11y_baseline.BaselineError as error:
|
|
186
|
+
raise HealError(f"the accessibility baseline can't be read ({type(error).__name__})") from None
|
|
187
|
+
if getattr(check, "a11y_scope", None) and baseline != check.a11y_baseline:
|
|
188
|
+
raise InputChanged("the accessibility baseline changed since check admission")
|
|
189
|
+
a11y_sha = _digest(sorted(baseline))
|
|
190
|
+
business_sha = None
|
|
191
|
+
if getattr(check, "business", None) is not None:
|
|
192
|
+
from ..verify.assertions import CheckError
|
|
193
|
+
from ..verify.business import load_business_binding
|
|
194
|
+
from ..verify.checks import project_root
|
|
195
|
+
try:
|
|
196
|
+
business_sha = load_business_binding(check, project_root(check.source)).digest
|
|
197
|
+
except (CheckError, OSError) as error:
|
|
198
|
+
raise HealError(f"the app-state fixture can't be read ({type(error).__name__})") from None
|
|
199
|
+
return {"visualSha256": None, "businessSha256": business_sha, "seedSha256": seed_sha,
|
|
200
|
+
"mediaSha256": media_sha, "pushSha256": push_sha, "a11ySha256": a11y_sha}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def discard(check_file, cache_dir, cache_name):
|
|
204
|
+
"""An explicit fresh recording supersedes a proposal for this cache key."""
|
|
205
|
+
document = read(check_file)
|
|
206
|
+
if document is None or document.get("cache") != cache_name:
|
|
207
|
+
return False
|
|
208
|
+
private = None
|
|
209
|
+
if document.get("version") == VERSION:
|
|
210
|
+
private = _proposal_file(cache_dir, cache_name, document.get("proposal"))
|
|
211
|
+
heal_file(check_file).unlink()
|
|
212
|
+
sync_directory(heal_file(check_file).parent)
|
|
213
|
+
if private is not None:
|
|
214
|
+
private.unlink(missing_ok=True)
|
|
215
|
+
sync_directory(private.parent)
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def record(check_file, heals, *, run_id, cache_file, candidate, base_revision, check_sha256,
|
|
220
|
+
effective_sha256, proof,
|
|
221
|
+
app=None, ios=None, device=None):
|
|
222
|
+
"""Stage a private candidate, then publish its review diff. The approved path is untouched."""
|
|
223
|
+
path = heal_file(check_file)
|
|
224
|
+
if path.exists():
|
|
225
|
+
raise HealError(f"{path.name} already waits for review")
|
|
226
|
+
validate(candidate)
|
|
227
|
+
cache_file = Path(cache_file)
|
|
228
|
+
if file_name(candidate["identity"], candidate["env"]) != cache_file.name \
|
|
229
|
+
or not isinstance(base_revision, str) or not PROPOSAL.fullmatch(base_revision):
|
|
230
|
+
raise HealError("the healed path doesn't match its approved recording")
|
|
231
|
+
from ..verify.checks import project_root
|
|
232
|
+
root = project_root(check_file)
|
|
233
|
+
at = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
234
|
+
entries = []
|
|
235
|
+
for heal in heals:
|
|
236
|
+
entry = {key_: heal.get(key_) for key_ in ("step", "from", "to", "why", "old", "new")}
|
|
237
|
+
entry["frames"] = {name: _from_root(value, root) for name, value in (heal.get("frames") or {}).items()}
|
|
238
|
+
entries.append(entry | {"run": run_id, "at": at})
|
|
239
|
+
check_sha = _check_digest(check_file)
|
|
240
|
+
if check_sha != check_sha256:
|
|
241
|
+
raise HealError("the check changed during healing; the approved path was kept")
|
|
242
|
+
from .checks import _saved_check
|
|
243
|
+
try:
|
|
244
|
+
current_check = _saved_check(check_file)
|
|
245
|
+
current_proof = proof_assets(current_check, include_a11y=proof["a11ySha256"] is not None)
|
|
246
|
+
except (OSError, ValueError) as error:
|
|
247
|
+
raise HealError(f"the check can't be read ({type(error).__name__})") from None
|
|
248
|
+
if current_proof != proof or effective_digest(current_check) != effective_sha256:
|
|
249
|
+
raise HealError("the check's evidence changed during healing; the approved path was kept")
|
|
250
|
+
payload = {"candidate": candidate, "baseRevision": base_revision, "checkSha256": check_sha,
|
|
251
|
+
"effectiveSha256": effective_sha256, "proof": proof, "heals": entries}
|
|
252
|
+
proposal = _digest(payload)
|
|
253
|
+
private = _proposal_file(cache_file.parent, cache_file.name, proposal)
|
|
254
|
+
atomic_write(private, json.dumps(payload, ensure_ascii=False))
|
|
255
|
+
document = {"version": VERSION, "check": Path(check_file).name, "run": run_id,
|
|
256
|
+
"cache": cache_file.name, "proposal": proposal,
|
|
257
|
+
"baseRevision": base_revision, "candidateRevision": revision(candidate),
|
|
258
|
+
"checkSha256": check_sha, "app": app, "ios": ios, "device": device, "heals": entries}
|
|
259
|
+
_write(path, document)
|
|
260
|
+
return path
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _from_root(value, root):
|
|
264
|
+
"""A frame's path relative to the project (``.mobster/runs/<run>/frames/...``), else as it is."""
|
|
265
|
+
if not value:
|
|
266
|
+
return None
|
|
267
|
+
try:
|
|
268
|
+
return Path(value).resolve().relative_to(Path(root).resolve()).as_posix()
|
|
269
|
+
except (ValueError, OSError):
|
|
270
|
+
return str(value)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _review(check_file, cache_dir, decision):
|
|
274
|
+
cache = Cache(cache_dir)
|
|
275
|
+
with cache.locked():
|
|
276
|
+
return _review_locked(check_file, cache, decision)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _review_locked(check_file, cache, decision):
|
|
280
|
+
document = read(check_file)
|
|
281
|
+
if document is None:
|
|
282
|
+
return 0
|
|
283
|
+
if document.get("version") != VERSION:
|
|
284
|
+
raise HealError("this older heal has no approved baseline; record the check again with --fresh")
|
|
285
|
+
name, proposal = document.get("cache"), document.get("proposal")
|
|
286
|
+
private = _proposal_file(cache.directory, name, proposal)
|
|
287
|
+
if (not isinstance(document.get("baseRevision"), str)
|
|
288
|
+
or not PROPOSAL.fullmatch(document["baseRevision"])
|
|
289
|
+
or not isinstance(document.get("checkSha256"), str)
|
|
290
|
+
or not PROPOSAL.fullmatch(document["checkSha256"])
|
|
291
|
+
or not isinstance(document.get("candidateRevision"), str)
|
|
292
|
+
or not PROPOSAL.fullmatch(document["candidateRevision"])):
|
|
293
|
+
raise HealError("the heal diff is invalid; review evidence was kept")
|
|
294
|
+
try:
|
|
295
|
+
payload = json.loads(private.read_text(encoding="utf-8"))
|
|
296
|
+
candidate = validate(payload["candidate"])
|
|
297
|
+
bound_name = file_name(candidate["identity"], candidate["env"])
|
|
298
|
+
except (OSError, ValueError, KeyError, TypeError, PathError):
|
|
299
|
+
raise HealError("the heal proposal is missing or invalid; review evidence was kept") from None
|
|
300
|
+
if (_digest(payload) != proposal or payload.get("baseRevision") != document.get("baseRevision")
|
|
301
|
+
or payload.get("checkSha256") != document.get("checkSha256")
|
|
302
|
+
or payload.get("heals") != document.get("heals")
|
|
303
|
+
or revision(candidate) != document.get("candidateRevision")
|
|
304
|
+
or bound_name != name or _check_digest(check_file) != document["checkSha256"]):
|
|
305
|
+
raise HealError("the heal proposal or check changed; review evidence was kept")
|
|
306
|
+
from .checks import _saved_check
|
|
307
|
+
try:
|
|
308
|
+
current_check = _saved_check(check_file)
|
|
309
|
+
expected_proof = payload.get("proof")
|
|
310
|
+
if not isinstance(expected_proof, dict):
|
|
311
|
+
raise HealError("the heal proposal has no evidence proof; review evidence was kept")
|
|
312
|
+
current_proof = proof_assets(current_check, include_a11y=expected_proof.get("a11ySha256") is not None)
|
|
313
|
+
except (OSError, ValueError) as error:
|
|
314
|
+
raise HealError(f"the check can't be read ({type(error).__name__}); review evidence was kept") from None
|
|
315
|
+
if current_proof != expected_proof or effective_digest(current_check) != payload.get("effectiveSha256"):
|
|
316
|
+
raise HealError("the check's evidence changed; review evidence was kept")
|
|
317
|
+
approved = cache.load(candidate["identity"], candidate["env"])
|
|
318
|
+
if approved is None:
|
|
319
|
+
raise HealError("the approved recording is missing or invalid; review evidence was kept")
|
|
320
|
+
receipt = approved.get("review")
|
|
321
|
+
if receipt is not None and not isinstance(receipt, dict):
|
|
322
|
+
raise HealError("the approved recording has an invalid review receipt; review evidence was kept")
|
|
323
|
+
receipt = receipt or {}
|
|
324
|
+
if receipt.get("proposal") == proposal:
|
|
325
|
+
if receipt.get("decision") != decision:
|
|
326
|
+
raise HealError("this heal already received the opposite decision")
|
|
327
|
+
else:
|
|
328
|
+
if revision(approved) != document["baseRevision"]:
|
|
329
|
+
raise HealError("the approved recording changed; review evidence was kept")
|
|
330
|
+
for heal in document["heals"]:
|
|
331
|
+
if (not isinstance(heal, dict) or not isinstance(heal.get("step"), int)
|
|
332
|
+
or isinstance(heal["step"], bool) or not 1 <= heal["step"] <= len(approved["steps"])
|
|
333
|
+
or not isinstance(heal.get("old"), list)):
|
|
334
|
+
raise HealError("the heal diff has an invalid step; review evidence was kept")
|
|
335
|
+
if decision == "accept":
|
|
336
|
+
chosen = dict(candidate)
|
|
337
|
+
else:
|
|
338
|
+
chosen = dict(approved)
|
|
339
|
+
strict = set(chosen.get("strict") or ())
|
|
340
|
+
for heal in document["heals"]:
|
|
341
|
+
start, old = heal["step"], heal["old"]
|
|
342
|
+
count = max(1, len(old))
|
|
343
|
+
strict.update(range(start, min(start + count, len(chosen["steps"]) + 1)))
|
|
344
|
+
chosen["strict"] = sorted(strict)
|
|
345
|
+
chosen["review"] = {"proposal": proposal, "decision": decision}
|
|
346
|
+
cache.save(chosen)
|
|
347
|
+
heal_file(check_file).unlink()
|
|
348
|
+
sync_directory(heal_file(check_file).parent)
|
|
349
|
+
private.unlink(missing_ok=True)
|
|
350
|
+
sync_directory(private.parent)
|
|
351
|
+
return len(document["heals"])
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def accept(check_file, cache_dir):
|
|
355
|
+
"""Make the reviewed proposal the active recording, after verifying its approved base."""
|
|
356
|
+
return _review(check_file, cache_dir, "accept")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def reject(check_file, cache_dir):
|
|
360
|
+
"""Keep the approved steps and protect the replaced interval from later healing."""
|
|
361
|
+
return _review(check_file, cache_dir, "reject")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
__all__ = ["HealError", "heal_file", "read", "record", "accept", "reject", "SUFFIX"]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""A recorded action path: what a passing run did, step by step, with what each step needs to find on the screen.
|
|
2
|
+
|
|
3
|
+
A step is a plain dict, so it goes to JSON and YAML as it is::
|
|
4
|
+
|
|
5
|
+
{"op": "TAP", "app": "com.example.app",
|
|
6
|
+
"target": {"label": "Continue", "role": "Button", "id": "/0/3/1", "frame": [.1, .8, .8, .06]},
|
|
7
|
+
"screen": {"hash": "9f2c...", "signature": [["Button", "Continue"], ["StaticText", "Welcome"]]},
|
|
8
|
+
"text": null, "param": null, "commit": null, "point": null, "live": false}
|
|
9
|
+
|
|
10
|
+
- ``target``: the element acted on (label, role, the tree path as ``id``, its frame in screen fractions).
|
|
11
|
+
- ``screen``: the screen before the action: state.py's structural signature (role/label pairs) and its hash.
|
|
12
|
+
Coordinate steps also hold its exact content fingerprint, including geometry.
|
|
13
|
+
- ``text``: typed text a replay may type again (a literal from the check's own steps), or None.
|
|
14
|
+
- ``param``: for a learned workflow, the name of the blank the typed text fills; ``text`` is then None.
|
|
15
|
+
- ``commit``: the act (harness_api.commit_act) when the step sends, posts, deletes, buys or submits.
|
|
16
|
+
- ``point``: (x, y) in screen fractions for TAP_XY and DISMISS, which have no element.
|
|
17
|
+
- ``live``: the step can't be replayed (text the model wrote, a macro read): the model does it each run.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# A screen's role/label pairs as short hashes: the frontier's StepRecord.shape uses the same rule.
|
|
21
|
+
from ..state import SHAPE_LIMIT, SIGNATURE_LIMIT, screen_shape, signature_hash # noqa: F401
|
|
22
|
+
|
|
23
|
+
VERSION = 1
|
|
24
|
+
MAX_STEPS = 60
|
|
25
|
+
MAX_ALTS = 4
|
|
26
|
+
|
|
27
|
+
ELEMENT_OPS = frozenset({"TAP", "LONG_PRESS", "TYPE", "TYPE_SUBMIT", "SET_TEXT", "SUBMIT"})
|
|
28
|
+
TEXT_OPS = frozenset({"TYPE", "TYPE_SUBMIT", "SET_TEXT"})
|
|
29
|
+
SWIPES = frozenset({"SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT"})
|
|
30
|
+
POINT_OPS = frozenset({"TAP_XY", "DISMISS"})
|
|
31
|
+
OPS = ELEMENT_OPS | SWIPES | POINT_OPS | frozenset({"LAUNCH_APP", "HOME", "WAIT"})
|
|
32
|
+
LIVE_OPS = frozenset({"READ_LIST", "SCROLL_TO"})
|
|
33
|
+
|
|
34
|
+
VERBS = {"TAP": "Tap", "LONG_PRESS": "Press and hold", "TYPE": "Type in", "TYPE_SUBMIT": "Type and submit in",
|
|
35
|
+
"SET_TEXT": "Set the text of", "SUBMIT": "Submit", "SWIPE_UP": "Swipe up", "SWIPE_DOWN": "Swipe down",
|
|
36
|
+
"SWIPE_LEFT": "Swipe left", "SWIPE_RIGHT": "Swipe right", "LAUNCH_APP": "Open",
|
|
37
|
+
"HOME": "Go to the Home Screen",
|
|
38
|
+
"TAP_XY": "Tap a point", "DISMISS": "Dismiss", "WAIT": "Wait", "READ_LIST": "Read the list",
|
|
39
|
+
"SCROLL_TO": "Scroll to"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PathError(ValueError):
|
|
43
|
+
"""A stored path that can't be used: it is ignored, and the next run records a new one."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def screen_hashes(snapshot):
|
|
47
|
+
"""{"hash", "shape", "app"} for ``snapshot`` as hashes, with no screen text; None when there's no screen."""
|
|
48
|
+
from ..state import signature
|
|
49
|
+
if snapshot is None:
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
pairs = signature(snapshot)
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
return {"hash": signature_hash(pairs), "shape": screen_shape(pairs),
|
|
56
|
+
"app": getattr(snapshot, "bundle_id", None) or None}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def target_of(element):
|
|
60
|
+
"""A step's ``target`` from a state.Element."""
|
|
61
|
+
if element is None:
|
|
62
|
+
return None
|
|
63
|
+
return {"label": element.label or "", "role": element.role or "", "id": element.locator or None,
|
|
64
|
+
"frame": [round(float(value), 4) for value in element.rect]}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def describe(step):
|
|
68
|
+
if not isinstance(step, dict):
|
|
69
|
+
return "an unknown step"
|
|
70
|
+
if isinstance(step.get("purpose"), str) and step["purpose"].strip():
|
|
71
|
+
return step["purpose"].strip()
|
|
72
|
+
op = step.get("op") or "?"
|
|
73
|
+
verb = VERBS.get(op, op.title())
|
|
74
|
+
target = step.get("target") or {}
|
|
75
|
+
label = target.get("label") if isinstance(target, dict) else None
|
|
76
|
+
if op == "LAUNCH_APP":
|
|
77
|
+
return f"{verb} {step.get('app') or step.get('bundle') or 'the app'}"
|
|
78
|
+
if label:
|
|
79
|
+
return f'{verb} "{label}"'
|
|
80
|
+
if op in ELEMENT_OPS and isinstance(target, dict) and target.get("role"):
|
|
81
|
+
return f"{verb} the {target['role']}"
|
|
82
|
+
return verb
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def anchor(step):
|
|
86
|
+
"""What a heal diff shows for a step: op, label and role (no tree path, no frame)."""
|
|
87
|
+
target = step.get("target") if isinstance(step.get("target"), dict) else {}
|
|
88
|
+
out = {"op": step.get("op")}
|
|
89
|
+
if target.get("label"):
|
|
90
|
+
out["label"] = target["label"]
|
|
91
|
+
if target.get("role"):
|
|
92
|
+
out["role"] = target["role"]
|
|
93
|
+
if step.get("op") == "LAUNCH_APP" and step.get("app"):
|
|
94
|
+
out["app"] = step["app"]
|
|
95
|
+
if step.get("param"):
|
|
96
|
+
out["text"] = "{" + step["param"] + "}"
|
|
97
|
+
elif step.get("text") is not None:
|
|
98
|
+
out["text"] = step["text"]
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _string(value, limit=500):
|
|
103
|
+
return isinstance(value, str) and len(value) <= limit
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def valid_step(step):
|
|
107
|
+
"""True for a step a replay can trust. A path with any invalid step is dropped as a whole."""
|
|
108
|
+
if not isinstance(step, dict):
|
|
109
|
+
return False
|
|
110
|
+
op = step.get("op")
|
|
111
|
+
if not isinstance(op, str):
|
|
112
|
+
return False
|
|
113
|
+
# A saved program's scroll to a blank's value (learned/programs.py) is kept, not live: the frontier runs it as its
|
|
114
|
+
# SCROLL_TO with the request's value. Nothing else replays it (Replayer.act refuses an op it can't run).
|
|
115
|
+
scroll = op == "SCROLL_TO" and _string(step.get("param"), 16) and step.get("target") is None
|
|
116
|
+
if op not in OPS and not scroll and not (step.get("live") is True and _string(op, 40) and op.isupper()):
|
|
117
|
+
return False
|
|
118
|
+
if step.get("app") is not None and not _string(step["app"], 255):
|
|
119
|
+
return False
|
|
120
|
+
target = step.get("target")
|
|
121
|
+
if target is not None:
|
|
122
|
+
if not isinstance(target, dict) or not _string(target.get("label", ""), 500) \
|
|
123
|
+
or not _string(target.get("role", ""), 500):
|
|
124
|
+
return False
|
|
125
|
+
if target.get("id") is not None and not _string(target["id"], 16000):
|
|
126
|
+
return False
|
|
127
|
+
frame = target.get("frame")
|
|
128
|
+
if frame is not None and (not isinstance(frame, list) or len(frame) != 4
|
|
129
|
+
or not all(isinstance(v, (int, float)) and 0 <= v <= 1.0001 for v in frame)):
|
|
130
|
+
return False
|
|
131
|
+
if op in ELEMENT_OPS and not step.get("live") and target is None:
|
|
132
|
+
return False
|
|
133
|
+
if op == "LAUNCH_APP" and not (_string(step.get("app"), 255) and "." in step["app"]):
|
|
134
|
+
return False
|
|
135
|
+
screen = step.get("screen")
|
|
136
|
+
if screen is not None:
|
|
137
|
+
if not isinstance(screen, dict) or not _string(screen.get("hash", ""), 64):
|
|
138
|
+
return False
|
|
139
|
+
signature = screen.get("signature")
|
|
140
|
+
if signature is not None and (not isinstance(signature, list) or len(signature) > SIGNATURE_LIMIT or not all(
|
|
141
|
+
isinstance(pair, list) and len(pair) == 2 and all(_string(v) for v in pair) for pair in signature)):
|
|
142
|
+
return False
|
|
143
|
+
shape = screen.get("shape")
|
|
144
|
+
if shape is not None and (not isinstance(shape, list) or len(shape) > SHAPE_LIMIT
|
|
145
|
+
or not all(_string(v, 16) for v in shape)):
|
|
146
|
+
return False
|
|
147
|
+
fingerprint = screen.get("fingerprint")
|
|
148
|
+
if fingerprint is not None and (not isinstance(fingerprint, str) or len(fingerprint) != 64
|
|
149
|
+
or any(char not in "0123456789abcdef" for char in fingerprint)):
|
|
150
|
+
return False
|
|
151
|
+
if step.get("text") is not None and not _string(step["text"], 4000):
|
|
152
|
+
return False
|
|
153
|
+
if step.get("param") is not None and not _string(step["param"], 40):
|
|
154
|
+
return False
|
|
155
|
+
if op in TEXT_OPS and not step.get("live") and step.get("text") is None and step.get("param") is None:
|
|
156
|
+
return False
|
|
157
|
+
point = step.get("point")
|
|
158
|
+
if point is not None and (not isinstance(point, list) or len(point) != 2
|
|
159
|
+
or not all(isinstance(v, (int, float)) and 0 <= v <= 1 for v in point)):
|
|
160
|
+
return False
|
|
161
|
+
if op in POINT_OPS and not step.get("live") and point is None:
|
|
162
|
+
return False
|
|
163
|
+
if step.get("commit") is not None and not _string(step["commit"], 40):
|
|
164
|
+
return False
|
|
165
|
+
# A taught step (learned/teach.py): what it is for, the person's rules for it, and other screens it was shown on.
|
|
166
|
+
if step.get("purpose") is not None and not _string(step["purpose"], 200):
|
|
167
|
+
return False
|
|
168
|
+
rules = step.get("rules")
|
|
169
|
+
if rules is not None and (not isinstance(rules, list) or len(rules) > 3 or not all(_string(r, 200) for r in rules)):
|
|
170
|
+
return False
|
|
171
|
+
# ``decide``: the person asked Mobster to choose this step each run (the model does it, by its purpose and rules).
|
|
172
|
+
if step.get("decide") is not None and not isinstance(step["decide"], bool):
|
|
173
|
+
return False
|
|
174
|
+
alts = step.get("alts")
|
|
175
|
+
if alts is not None and (not isinstance(alts, list) or len(alts) > MAX_ALTS or not all(
|
|
176
|
+
isinstance(alt, dict) and _alt_ok(step, alt) for alt in alts)):
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _alt_ok(step, alt):
|
|
182
|
+
op = alt.get("op")
|
|
183
|
+
if op in ("TAP", "TAP_XY", "LONG_PRESS"):
|
|
184
|
+
return valid_step({**step, **alt, "alts": None, "param": None, "text": None})
|
|
185
|
+
if op == step.get("op") and (op in SWIPES or op in TEXT_OPS):
|
|
186
|
+
return valid_step({**step, **alt, "alts": None})
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def validate(data):
|
|
191
|
+
"""A stored path dict, checked; raises PathError. Returns it."""
|
|
192
|
+
if not isinstance(data, dict) or data.get("version") != VERSION:
|
|
193
|
+
raise PathError("not a replay path of this version")
|
|
194
|
+
steps = data.get("steps")
|
|
195
|
+
if not isinstance(steps, list) or not 0 < len(steps) <= MAX_STEPS:
|
|
196
|
+
raise PathError(f"a path has 1 to {MAX_STEPS} steps")
|
|
197
|
+
for index, step in enumerate(steps):
|
|
198
|
+
if not valid_step(step):
|
|
199
|
+
raise PathError(f"step {index + 1} is not a step a replay can trust")
|
|
200
|
+
final = data.get("final")
|
|
201
|
+
if final is not None and (not isinstance(final, dict) or not _string(final.get("hash", ""), 64)
|
|
202
|
+
or final.get("app") is not None and not _string(final["app"], 255)
|
|
203
|
+
or final.get("shape") is not None and (
|
|
204
|
+
not isinstance(final["shape"], list) or len(final["shape"]) > SHAPE_LIMIT
|
|
205
|
+
or not all(_string(v, 16) for v in final["shape"]))):
|
|
206
|
+
raise PathError("the final screen is not valid")
|
|
207
|
+
strict = data.get("strict", [])
|
|
208
|
+
if not isinstance(strict, list) or not all(isinstance(i, int) and not isinstance(i, bool) for i in strict):
|
|
209
|
+
raise PathError("strict must be a list of step numbers")
|
|
210
|
+
return data
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def new_path(steps, *, kind, identity, env, final=None, source_run=None, created_at=None):
|
|
214
|
+
"""A path dict from recorded ``steps``."""
|
|
215
|
+
import time
|
|
216
|
+
return {"version": VERSION, "kind": kind, "identity": identity, "env": dict(env), "steps": list(steps),
|
|
217
|
+
"final": final, "source_run": source_run, "created_at": created_at or round(time.time(), 3),
|
|
218
|
+
"strict": [], "replays": 0}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def first_run(run_id, seconds, cost_usd, at=None):
|
|
222
|
+
"""What the run a path was first learned from took; ``costUsd`` is None when unreported, never a guess."""
|
|
223
|
+
import time
|
|
224
|
+
if not isinstance(seconds, (int, float)) or isinstance(seconds, bool) or not 0 <= seconds < 86_400:
|
|
225
|
+
return None
|
|
226
|
+
cost = cost_usd if isinstance(cost_usd, (int, float)) and not isinstance(cost_usd, bool) and cost_usd >= 0 else None
|
|
227
|
+
return {"runId": run_id if isinstance(run_id, str) else None, "seconds": round(float(seconds), 2),
|
|
228
|
+
"costUsd": round(float(cost), 5) if cost is not None else None, "at": round(at or time.time(), 3)}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def first_of(path, given=None):
|
|
232
|
+
kept = (path or {}).get("first") if isinstance(path, dict) else None
|
|
233
|
+
for value in (kept, given):
|
|
234
|
+
if isinstance(value, dict) and isinstance(value.get("seconds"), (int, float)):
|
|
235
|
+
return first_run(value.get("runId"), value["seconds"], value.get("costUsd"), value.get("at"))
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def replayable_count(path):
|
|
240
|
+
return sum(1 for step in path.get("steps") or () if not step.get("live"))
|