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,84 @@
|
|
|
1
|
+
"""What the picture shows: which rows a screen's bars hide, and the row a recorded value hides under."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .contract_screen import ATOM, NUMBER, as_float, center_y, element_text, plain
|
|
6
|
+
|
|
7
|
+
# Bars and sticky footers sit in the screen's bottom band (from BOTTOM_BAND of its height down). A row a bar lies over
|
|
8
|
+
# by more than OVER_ROW, or that the screen's foot cuts (rows are clipped to the screen), is in the tree but not all in
|
|
9
|
+
# the picture. A row taller than TALL_ROW is a pane: no scroll brings all of it into view. Rows hidden under a top bar
|
|
10
|
+
# are not counted: no stroke reliably starts inside the content under a header (TeamChat's pinned chips took both the
|
|
11
|
+
# drag's and the glide's start, live, 18-19 Oct).
|
|
12
|
+
BOTTOM_BAND, OVER_ROW, TALL_ROW, EDGE = .8, .005, .5, .005
|
|
13
|
+
BAR_ROLES = frozenset({"NavigationBar", "TabBar", "Toolbar"})
|
|
14
|
+
# A control this wide in the band is a bar (a sheet's "Request CityRideX" footer), and so is one of a line of BAR_LINE
|
|
15
|
+
# controls spanning as much (a SwiftUI tab bar is only its buttons, centres within BAR_LINE_Y).
|
|
16
|
+
BAR_WIDE, BAR_LINE, BAR_LINE_Y = .6, 3, .02
|
|
17
|
+
# A name in a recorded value: two or more capitalized words ("Seaport Raw Bar", "Rowan Byrne").
|
|
18
|
+
NAMES = re.compile(r"\b[A-Z][a-z]+(?: [A-Z][a-z]+)+")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def value_atoms(value):
|
|
22
|
+
"""What a screen row must hold to be a recorded value's source: its numbers, codes, times (ATOM) and names."""
|
|
23
|
+
return ATOM.findall(value or "") + NAMES.findall(value or "")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def holds_atom(atom, text):
|
|
27
|
+
"""Whether ``text`` (plain) holds ``atom``: a number as an equal number ("$1,200" in "1200.00"), else its text."""
|
|
28
|
+
if atom.startswith("$") or re.fullmatch(r"\d[\d,]*(?:\.\d+)?", atom):
|
|
29
|
+
number = as_float(atom.lstrip("$").strip())
|
|
30
|
+
return number is not None and any(abs((as_float(n) or -1e18) - number) < .005 for n in NUMBER.findall(text))
|
|
31
|
+
return plain(atom) in text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def within(inner, outer, slack=.005):
|
|
35
|
+
ix, iy, iw, ih = inner
|
|
36
|
+
ox, oy, ow, oh = outer
|
|
37
|
+
return ix >= ox - slack and iy >= oy - slack and ix + iw <= ox + ow + slack and iy + ih <= oy + oh + slack
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def screen_bars(elements):
|
|
41
|
+
"""[(index, bar)]: the bars and sticky footers of the screen's bottom band, by their place in ``elements``
|
|
42
|
+
(document order, back to front). A tab bar or toolbar, a control BAR_WIDE wide, or one of a line of controls as
|
|
43
|
+
wide; texts and pictures are content, never a bar."""
|
|
44
|
+
banded = [(index, element) for index, element in enumerate(elements)
|
|
45
|
+
if element.role not in ("StaticText", "Image") and element.rect[2] > 0 and element.rect[3] > 0
|
|
46
|
+
and element.rect[1] >= BOTTOM_BAND]
|
|
47
|
+
bars = []
|
|
48
|
+
for index, element in banded:
|
|
49
|
+
line = [other for _, other in banded if abs(center_y(other) - center_y(element)) <= BAR_LINE_Y]
|
|
50
|
+
if (element.role in BAR_ROLES or element.rect[2] >= BAR_WIDE or len(line) >= BAR_LINE
|
|
51
|
+
and max(o.rect[0] + o.rect[2] for o in line) - min(o.rect[0] for o in line) >= BAR_WIDE):
|
|
52
|
+
bars.append((index, element))
|
|
53
|
+
return bars
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def hidden_below(row, index, bars):
|
|
57
|
+
"""Whether the screen's foot, or a bar of its bottom band (screen_bars), lies over part of ``row`` (at ``index`` in
|
|
58
|
+
the screen's elements). A tab bar or toolbar lies over any row not inside it; any other bar only over rows drawn
|
|
59
|
+
before it (its own texts come after it). iOSWorld multi-069 (18 Oct): "Comfort, $22.40" and its "$22.40" were
|
|
60
|
+
read from the tree under the ride sheet's "Request CityRideX" footer, and no picture of the run showed them."""
|
|
61
|
+
x, y, w, h = row.rect
|
|
62
|
+
if w <= 0 or h <= 0 or h > TALL_ROW:
|
|
63
|
+
return False
|
|
64
|
+
if y + h >= 1 - EDGE:
|
|
65
|
+
return True
|
|
66
|
+
return any(bar is not row and min(x + w, bx + bw) > max(x, bx) and min(y + h, by + bh) - max(y, by) > OVER_ROW
|
|
67
|
+
and (not within(row.rect, bar.rect) if bar.role in BAR_ROLES else at > index)
|
|
68
|
+
for at, bar in bars for bx, by, bw, bh in [bar.rect])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def hidden_row(items, elements, step):
|
|
72
|
+
"""The row of ``elements`` that holds a value marked at ``step`` when every row holding it is hidden below
|
|
73
|
+
(hidden_below), else None. The agent scrolls that row into view before it leaves the screen, so the run's frames
|
|
74
|
+
show what its answer cites."""
|
|
75
|
+
bars = None
|
|
76
|
+
for item in items:
|
|
77
|
+
if item.value_step != step or not item.value or item.kind == "REPORT" and item.status != "found":
|
|
78
|
+
continue
|
|
79
|
+
bars = bars if bars is not None else screen_bars(elements)
|
|
80
|
+
for atom in value_atoms(item.value):
|
|
81
|
+
rows = [(i, e) for i, e in enumerate(elements) if holds_atom(atom, plain(element_text(e)))]
|
|
82
|
+
if rows and all(hidden_below(row, i, bars) for i, row in rows):
|
|
83
|
+
return rows[0][1]
|
|
84
|
+
return None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The opt-in screen-to-action corpus, dark behind ``MOBSTER_EXPERIMENTAL_CORPUS`` (default off).
|
|
2
|
+
|
|
3
|
+
Every completed run is a list of (screen, action) pairs on real iOS. With the person's opt-in (``consent``), the
|
|
4
|
+
run listener writes one record per step to a local JSONL file (``store``): the screen as roles and hashed labels
|
|
5
|
+
(``skeleton``), the action without its text, the app and version, the outcome. Nothing leaves the Mac: ``mobster
|
|
6
|
+
corpus export`` writes a masked bundle for the person to read, and sharing is a later decision.
|
|
7
|
+
|
|
8
|
+
With the flag off, or the opt-in off, the hooks return what they were given and no file is made.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
ENV_FLAG = "MOBSTER_EXPERIMENTAL_CORPUS"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def enabled(env=None):
|
|
17
|
+
"""Whether the corpus is switched on at all (``MOBSTER_EXPERIMENTAL_CORPUS=1``)."""
|
|
18
|
+
env = os.environ if env is None else env
|
|
19
|
+
return (env.get(ENV_FLAG) or "").strip().casefold() in ("1", "true", "yes", "on")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def install(hooks):
|
|
23
|
+
"""Add ``mobster corpus`` to the engine's commands, only with the flag on (harness_api._load calls this)."""
|
|
24
|
+
if enabled():
|
|
25
|
+
from . import cli
|
|
26
|
+
hooks.cli.append(cli)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""`mobster corpus` (only with MOBSTER_EXPERIMENTAL_CORPUS=1): the opt-in, what is stored, and an export to read.
|
|
2
|
+
|
|
3
|
+
Added to the engine's commands by corpus.install as an extension: ``add_arguments(subs, commands)`` and
|
|
4
|
+
``run(args, output)``, which returns an exit code (0 done, 1 refused, 2 usage) or None for another command.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import consent, store
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_arguments(subs, commands):
|
|
14
|
+
parser = subs.add_parser("corpus", prog="mobster corpus", help="keep and inspect Mobster's private run corpus",
|
|
15
|
+
description="Keep a private record of each finished task's screens and actions on this "
|
|
16
|
+
"Mac, if you opt in, and read it. Nothing is uploaded.")
|
|
17
|
+
sub = parser.add_subparsers(dest="corpus_command", metavar="<subcommand>")
|
|
18
|
+
for name, help in (("status", "say whether the corpus is on"), ("off", "turn the corpus off"),
|
|
19
|
+
("stats", "count what is stored")):
|
|
20
|
+
sub.add_parser(name, help=help).add_argument("--json", action="store_true", help="print JSON")
|
|
21
|
+
on = sub.add_parser("on", help="show what is kept and turn the corpus on",
|
|
22
|
+
description=consent.TEXT)
|
|
23
|
+
on.add_argument("--yes", action="store_true", help="answer yes to the question after the text is shown")
|
|
24
|
+
export = sub.add_parser("export", help="write the stored records to a file you can read")
|
|
25
|
+
export.add_argument("--out", required=True, metavar="FILE", help="a new file to write")
|
|
26
|
+
commands["corpus"] = parser
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run(args, output):
|
|
30
|
+
if getattr(args, "command", None) != "corpus":
|
|
31
|
+
return None
|
|
32
|
+
word = getattr(args, "corpus_command", None) or "status"
|
|
33
|
+
as_json = getattr(args, "json", False)
|
|
34
|
+
try:
|
|
35
|
+
if word == "on":
|
|
36
|
+
print(consent.TEXT)
|
|
37
|
+
if not args.yes:
|
|
38
|
+
if not sys.stdin.isatty() or input("\nTurn it on? Type yes: ").strip().lower() != "yes":
|
|
39
|
+
print("Not turned on.", file=sys.stderr)
|
|
40
|
+
return 1
|
|
41
|
+
consent.set_on(True)
|
|
42
|
+
print("\nThe corpus is on. `mobster corpus off` turns it off.")
|
|
43
|
+
elif word == "off":
|
|
44
|
+
consent.set_on(False)
|
|
45
|
+
print("The corpus is off. What is stored stays until you delete the corpus folder.")
|
|
46
|
+
elif word == "status":
|
|
47
|
+
state = {"on": consent.on(), "folder": str(store.directory())}
|
|
48
|
+
print(json.dumps(state) if as_json else f"The corpus is {'on' if state['on'] else 'off'}.")
|
|
49
|
+
elif word == "stats":
|
|
50
|
+
data = store.stats()
|
|
51
|
+
print(json.dumps(data) if as_json else
|
|
52
|
+
f"{data['records']} steps from {data['runs']} runs, {data['bytes']} of {data['capBytes']} bytes "
|
|
53
|
+
f"in {data['files']} files, in {store.directory()}")
|
|
54
|
+
elif word == "export":
|
|
55
|
+
count = store.export(args.out)
|
|
56
|
+
print(f"Wrote {count} records to {args.out}. Open it to see exactly what is stored.")
|
|
57
|
+
else:
|
|
58
|
+
print("mobster corpus: use on, off, status, stats or export", file=sys.stderr)
|
|
59
|
+
return 2
|
|
60
|
+
except (store.CorpusError, OSError) as error:
|
|
61
|
+
print(f"mobster corpus: {error}", file=sys.stderr)
|
|
62
|
+
return 1
|
|
63
|
+
return 0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The opt-in: off until the person has been shown ``TEXT`` and said yes, and off again when the text changes.
|
|
2
|
+
|
|
3
|
+
The setting is ``corpus/consent.json`` in the data folder, which Mobster for Mac and ``mobster corpus on`` write the
|
|
4
|
+
same way. No file, an unreadable file or an older text version is off.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from ..paths import user_data_dir, write_text_atomic
|
|
11
|
+
|
|
12
|
+
TEXT_VERSION = 1
|
|
13
|
+
TITLE = "Help Mobster learn from your runs"
|
|
14
|
+
TEXT = f"""{TITLE}
|
|
15
|
+
|
|
16
|
+
When this is on, Mobster keeps a private record of each task that finishes, one line per step, on this Mac only.
|
|
17
|
+
Nothing is uploaded or shared. You can read everything it keeps with `mobster corpus export`, and turn it off at
|
|
18
|
+
any time.
|
|
19
|
+
|
|
20
|
+
What a record holds:
|
|
21
|
+
- the shape of the screen: each control's kind (Button, Cell, TextField) and a one-way hash of its label, with
|
|
22
|
+
numbers folded; a label that looks like an email, phone number, handle or link is replaced by its kind
|
|
23
|
+
- the action: tap, type, swipe (not the text typed), and which control it hit
|
|
24
|
+
- the app's bundle id and version, whether the screen changed, and a random id for the run
|
|
25
|
+
|
|
26
|
+
What it never holds: screenshots, text you typed, names, handles, numbers, messages, or any screen text. Steps in
|
|
27
|
+
Passwords, Wallet and Health, on a screen with a password field, and in a task you redirected or that did not finish
|
|
28
|
+
are not kept."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _path():
|
|
32
|
+
return user_data_dir() / "corpus" / "consent.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def on():
|
|
36
|
+
"""True when the person opted in to the current ``TEXT``."""
|
|
37
|
+
try:
|
|
38
|
+
state = json.loads(_path().read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, ValueError):
|
|
40
|
+
return False
|
|
41
|
+
return isinstance(state, dict) and state.get("optedIn") is True and state.get("textVersion") == TEXT_VERSION
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_on(value):
|
|
45
|
+
"""Record the answer to ``TEXT``; turning it on is only ever done after the text was shown."""
|
|
46
|
+
state = {"optedIn": bool(value), "textVersion": TEXT_VERSION, "at": int(time.time())}
|
|
47
|
+
_path().parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
48
|
+
write_text_atomic(_path(), json.dumps(state))
|
|
49
|
+
_path().chmod(0o600)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def active():
|
|
53
|
+
"""The flag is on and the person opted in: the only state in which anything is captured or written."""
|
|
54
|
+
from . import enabled
|
|
55
|
+
return enabled() and on()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""The run listener: keeps a run's steps in memory and writes them only when the run finished done, unredirected.
|
|
2
|
+
|
|
3
|
+
A step is kept only while ``consent.active()``; with the corpus off a run costs this listener nothing (no state,
|
|
4
|
+
no file). The skeleton comes from the step itself (``StepRecord.corpus``, filled by frontier/hooks.py), so nothing
|
|
5
|
+
here reads the phone.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from ..errors import MobsterError
|
|
12
|
+
from ..frontier.protocol import TYPED_OPS
|
|
13
|
+
from . import consent, store
|
|
14
|
+
from .skeleton import fingerprint
|
|
15
|
+
|
|
16
|
+
DENY = frozenset({"com.apple.Passwords", "com.apple.Passbook", "com.apple.Health"})
|
|
17
|
+
MAX_STEPS = 400
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CorpusListener:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._runs = {}
|
|
23
|
+
self._lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
def on_created(self, run, runtime):
|
|
26
|
+
if consent.active():
|
|
27
|
+
with self._lock:
|
|
28
|
+
self._runs[run.id] = {"steps": [], "redirected": False, "runtime": runtime}
|
|
29
|
+
|
|
30
|
+
def on_step(self, run, record):
|
|
31
|
+
captured = getattr(record, "corpus", None)
|
|
32
|
+
with self._lock:
|
|
33
|
+
held = self._runs.get(getattr(run, "id", None))
|
|
34
|
+
if held is None:
|
|
35
|
+
return
|
|
36
|
+
if getattr(record, "approval", None) == "redirected":
|
|
37
|
+
held["redirected"] = True
|
|
38
|
+
if (captured is None or record.app in DENY or not record.app or len(held["steps"]) >= MAX_STEPS
|
|
39
|
+
or not consent.active()):
|
|
40
|
+
return
|
|
41
|
+
held["steps"].append((record, captured))
|
|
42
|
+
|
|
43
|
+
def on_finished(self, run, summary, private):
|
|
44
|
+
with self._lock:
|
|
45
|
+
held = self._runs.pop(getattr(run, "id", None), None)
|
|
46
|
+
done = (summary or {}).get("status") == "completed" and (summary or {}).get("outcome") == "done"
|
|
47
|
+
if held is None or held["redirected"] or not done or not held["steps"] or not consent.active():
|
|
48
|
+
return
|
|
49
|
+
rid = hashlib.sha256(f"corpus:{run.id}".encode()).hexdigest()[:16]
|
|
50
|
+
versions = {}
|
|
51
|
+
records = []
|
|
52
|
+
for number, (step, captured) in enumerate(held["steps"]):
|
|
53
|
+
if step.app not in versions:
|
|
54
|
+
versions[step.app] = _version(held["runtime"], step.app)
|
|
55
|
+
target = step.target if isinstance(step.target, dict) and captured["target"] is not None else None
|
|
56
|
+
nodes = captured["nodes"]
|
|
57
|
+
index = captured["target"]
|
|
58
|
+
records.append({
|
|
59
|
+
"v": store.SCHEMA, "run": rid, "step": number, "app": step.app, "version": versions[step.app],
|
|
60
|
+
"screen": {"fp": fingerprint(nodes), "nodes": nodes},
|
|
61
|
+
"action": {"op": step.op, "typed": step.op in TYPED_OPS,
|
|
62
|
+
"target": {"i": index, "role": nodes[index][0], "label": nodes[index][1]}
|
|
63
|
+
if target is not None else None},
|
|
64
|
+
"outcome": {"changed": bool(step.changed), "run": "done"}})
|
|
65
|
+
try:
|
|
66
|
+
store.append(records)
|
|
67
|
+
except (OSError, store.CorpusError, MobsterError):
|
|
68
|
+
pass # the corpus is a courtesy: a full disk or a bad folder never touches a run
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _version(runtime, bundle):
|
|
72
|
+
from ..packs.core import app_version
|
|
73
|
+
try:
|
|
74
|
+
return app_version(runtime, bundle)
|
|
75
|
+
except Exception: # noqa: BLE001 -- no version is a valid answer
|
|
76
|
+
return None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""A screen as a skeleton: each control's role and a one-way hash of its label. No screen text survives.
|
|
2
|
+
|
|
3
|
+
Labels are the risk: a label can be a name, a handle, a number or a message. So a label is
|
|
4
|
+
- empty -> "";
|
|
5
|
+
- long (prose: a message, a caption, a title) -> "~long";
|
|
6
|
+
- an email, phone number, handle or link, or anything secret_filter would mask -> "~email", "~number", "~handle",
|
|
7
|
+
"~link", "~secret";
|
|
8
|
+
- otherwise folded (case, digit runs to "#") and hashed to 10 hex digits (compiled_read/routes.py's digest), so
|
|
9
|
+
"General" is the same token on every Mac and "3 new messages" equals "5 new messages".
|
|
10
|
+
A short name ("Sam") is a short label and hashes to a token anyone can test a guess against: why the corpus stays
|
|
11
|
+
local until sharing is decided.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import re
|
|
16
|
+
import unicodedata
|
|
17
|
+
|
|
18
|
+
from ..secret_filter import is_secret
|
|
19
|
+
|
|
20
|
+
MAX_NODES = 80
|
|
21
|
+
MAX_LABEL = 48
|
|
22
|
+
EMAIL = re.compile(r"\S+@\S+\.\S+")
|
|
23
|
+
HANDLE = re.compile(r"(?<!\w)@\w{2,}")
|
|
24
|
+
LINK = re.compile(r"(?i)https?://|www\.|\b[\w-]+\.(?:com|org|net|io|app|dev|me)\b")
|
|
25
|
+
DIGITS = re.compile(r"\d+")
|
|
26
|
+
PHONE = re.compile(r"(?<!\w)\+?\d[\d\s().-]{5,}\d")
|
|
27
|
+
ROLE = re.compile(r"[A-Za-z]{1,40}")
|
|
28
|
+
TOKEN = re.compile(r"[0-9a-f]{10}|~(?:long|email|number|handle|link|secret)|")
|
|
29
|
+
# Roles of a field that holds a password: the whole step is dropped, not masked.
|
|
30
|
+
SECURE = "secure"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def label_token(label):
|
|
34
|
+
text = unicodedata.normalize("NFKC", label or "").strip()
|
|
35
|
+
if not text:
|
|
36
|
+
return ""
|
|
37
|
+
if len(text) > MAX_LABEL:
|
|
38
|
+
return "~long"
|
|
39
|
+
if is_secret(text):
|
|
40
|
+
return "~secret"
|
|
41
|
+
for name, pattern in (("email", EMAIL), ("link", LINK), ("handle", HANDLE), ("number", PHONE)):
|
|
42
|
+
if pattern.search(text):
|
|
43
|
+
return "~" + name
|
|
44
|
+
if sum(map(len, DIGITS.findall(text))) >= 7:
|
|
45
|
+
return "~number"
|
|
46
|
+
return hashlib.sha1(DIGITS.sub("#", text.casefold()).encode()).hexdigest()[:10]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _role(role):
|
|
50
|
+
return role if ROLE.fullmatch(role or "") else "Other"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def capture(snapshot, element):
|
|
54
|
+
"""``{"nodes": [[role, token], ...], "target": index or None}`` for a screen and the control acted on; ``None``
|
|
55
|
+
when nothing may be kept of it: the corpus is off, or the screen has a secure field."""
|
|
56
|
+
from .consent import active
|
|
57
|
+
if not active():
|
|
58
|
+
return None
|
|
59
|
+
elements = list(getattr(snapshot, "elements", None) or ())
|
|
60
|
+
if any(SECURE in str(getattr(item, "role", "")).casefold() for item in elements):
|
|
61
|
+
return None
|
|
62
|
+
nodes, target = [], None
|
|
63
|
+
for item in elements[:MAX_NODES]:
|
|
64
|
+
if element is not None and target is None and getattr(item, "id", None) == element.id:
|
|
65
|
+
target = len(nodes)
|
|
66
|
+
nodes.append([_role(str(getattr(item, "role", ""))), label_token(getattr(item, "label", ""))])
|
|
67
|
+
return {"nodes": nodes, "target": target}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def fingerprint(nodes):
|
|
71
|
+
"""16 hex digits for a skeleton: the same screen shape on any Mac gives the same value."""
|
|
72
|
+
return hashlib.sha1(repr(sorted(map(tuple, nodes))).encode()).hexdigest()[:16]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""The corpus on disk: ``corpus/records.jsonl`` in the data folder (0700 folder, 0600 files), capped and rotated.
|
|
2
|
+
|
|
3
|
+
``records.jsonl`` takes appends until it reaches FILE_BYTES; then it becomes ``records.1.jsonl``, that one
|
|
4
|
+
``records.2.jsonl``, and the oldest is deleted. The corpus never holds more than FILES * FILE_BYTES.
|
|
5
|
+
``sanitize`` is the one gate: a record is written, and exported, only if every field has the shape below.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import fcntl
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import stat
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from ..paths import user_data_dir
|
|
16
|
+
from ..secret_filter import redact_deep
|
|
17
|
+
from .skeleton import MAX_NODES, ROLE, TOKEN
|
|
18
|
+
|
|
19
|
+
FILE_BYTES = 4_000_000
|
|
20
|
+
FILES = 3
|
|
21
|
+
SCHEMA = 1
|
|
22
|
+
OPS = re.compile(r"[A-Z_]{2,24}")
|
|
23
|
+
HASH = re.compile(r"[0-9a-f]{16}")
|
|
24
|
+
BUNDLE = re.compile(r"[A-Za-z0-9.-]{1,155}")
|
|
25
|
+
VERSION = re.compile(r"[0-9A-Za-z.+-]{1,40}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CorpusError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def directory():
|
|
33
|
+
return user_data_dir() / "corpus"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _private(path):
|
|
37
|
+
info = path.lstat()
|
|
38
|
+
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid():
|
|
39
|
+
raise CorpusError("the corpus folder must be a folder you own")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _files():
|
|
43
|
+
root = directory()
|
|
44
|
+
return [root / ("records.jsonl" if n == 0 else f"records.{n}.jsonl") for n in range(FILES)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _node(node):
|
|
48
|
+
return (isinstance(node, list) and len(node) == 2 and isinstance(node[0], str) and ROLE.fullmatch(node[0])
|
|
49
|
+
and isinstance(node[1], str) and TOKEN.fullmatch(node[1]))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sanitize(record):
|
|
53
|
+
"""``record`` as the schema allows it, or None. Every string is a fixed-shape token or passes secret_filter."""
|
|
54
|
+
try:
|
|
55
|
+
screen, action, outcome = record["screen"], record["action"], record["outcome"]
|
|
56
|
+
target = action["target"]
|
|
57
|
+
ok = (record["v"] == SCHEMA and HASH.fullmatch(record["run"]) and isinstance(record["step"], int)
|
|
58
|
+
and 0 <= record["step"] < 10_000 and BUNDLE.fullmatch(record["app"])
|
|
59
|
+
and (record["version"] is None or VERSION.fullmatch(record["version"]))
|
|
60
|
+
and HASH.fullmatch(screen["fp"]) and isinstance(screen["nodes"], list)
|
|
61
|
+
and len(screen["nodes"]) <= MAX_NODES and all(_node(n) for n in screen["nodes"])
|
|
62
|
+
and OPS.fullmatch(action["op"]) and isinstance(action["typed"], bool)
|
|
63
|
+
and (target is None or (isinstance(target["i"], int) and 0 <= target["i"] < len(screen["nodes"])
|
|
64
|
+
and _node([target["role"], target["label"]])))
|
|
65
|
+
and isinstance(outcome["changed"], bool) and outcome["run"] == "done")
|
|
66
|
+
except (KeyError, TypeError):
|
|
67
|
+
return None
|
|
68
|
+
return record if ok and redact_deep(record) == record else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def append(records):
|
|
72
|
+
"""Append ``records`` (already sanitized) as one write; returns how many were written."""
|
|
73
|
+
lines = "".join(json.dumps(r, separators=(",", ":")) + "\n" for r in records if sanitize(r))
|
|
74
|
+
if not lines:
|
|
75
|
+
return 0
|
|
76
|
+
root = directory()
|
|
77
|
+
root.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
root.mkdir(mode=0o700, exist_ok=True)
|
|
79
|
+
_private(root)
|
|
80
|
+
files = _files()
|
|
81
|
+
lock = os.open(root / ".lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
82
|
+
try:
|
|
83
|
+
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
84
|
+
if files[0].exists() and files[0].stat().st_size + len(lines) > FILE_BYTES:
|
|
85
|
+
files[-1].unlink(missing_ok=True)
|
|
86
|
+
for newer, older in reversed(list(zip(files, files[1:]))):
|
|
87
|
+
if newer.exists():
|
|
88
|
+
os.replace(newer, older)
|
|
89
|
+
fd = os.open(files[0], os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
|
90
|
+
with os.fdopen(fd, "a", encoding="utf-8") as handle:
|
|
91
|
+
handle.write(lines)
|
|
92
|
+
handle.flush()
|
|
93
|
+
os.fsync(handle.fileno())
|
|
94
|
+
finally:
|
|
95
|
+
os.close(lock)
|
|
96
|
+
return lines.count("\n")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read():
|
|
100
|
+
"""Every stored record that still passes ``sanitize``, oldest file first."""
|
|
101
|
+
for path in reversed(_files()):
|
|
102
|
+
try:
|
|
103
|
+
handle = open(path, encoding="utf-8")
|
|
104
|
+
except OSError:
|
|
105
|
+
continue
|
|
106
|
+
with handle:
|
|
107
|
+
for line in handle:
|
|
108
|
+
try:
|
|
109
|
+
record = sanitize(json.loads(line))
|
|
110
|
+
except ValueError:
|
|
111
|
+
continue
|
|
112
|
+
if record:
|
|
113
|
+
yield record
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def stats():
|
|
117
|
+
"""Counts for ``mobster corpus stats``: records, runs, steps per app, bytes, files."""
|
|
118
|
+
records, runs, apps = 0, set(), {}
|
|
119
|
+
for record in read():
|
|
120
|
+
records += 1
|
|
121
|
+
runs.add(record["run"])
|
|
122
|
+
apps[record["app"]] = apps.get(record["app"], 0) + 1
|
|
123
|
+
present = [p for p in _files() if p.exists()]
|
|
124
|
+
return {"records": records, "runs": len(runs), "bytes": sum(p.stat().st_size for p in present),
|
|
125
|
+
"files": len(present), "capBytes": FILE_BYTES * FILES, "apps": dict(sorted(apps.items()))}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def export(out):
|
|
129
|
+
"""Write the masked bundle to ``out`` (a new file, 0600): a header line, then one record per line."""
|
|
130
|
+
out = Path(out).expanduser()
|
|
131
|
+
records = list(read())
|
|
132
|
+
header = {"bundle": "mobster-corpus", "v": SCHEMA, "records": len(records),
|
|
133
|
+
"note": "roles, hashed labels and actions only; no screen text, no typed text"}
|
|
134
|
+
try:
|
|
135
|
+
fd = os.open(out, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
136
|
+
except FileExistsError:
|
|
137
|
+
raise CorpusError(f"{out} exists; choose a path that doesn't") from None
|
|
138
|
+
except OSError as error:
|
|
139
|
+
raise CorpusError(f"{out} can't be written ({error.strerror})") from None
|
|
140
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
141
|
+
for item in [header, *records]:
|
|
142
|
+
handle.write(json.dumps(item, separators=(",", ":")) + "\n")
|
|
143
|
+
return len(records)
|