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,326 @@
|
|
|
1
|
+
"""The files track's HTTP routes (seam S3; documented in docs/files.mdx)."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..api_errors import APIError
|
|
8
|
+
from ..api_routes import Response, Route
|
|
9
|
+
from ..phone_io import PhoneIOError
|
|
10
|
+
from . import service
|
|
11
|
+
from .store import FILE_MAX, ID, StoreError
|
|
12
|
+
|
|
13
|
+
DESTINATION_APP = "app:"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Copied(Exception):
|
|
17
|
+
def __init__(self, result):
|
|
18
|
+
self.result = result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _store(request):
|
|
22
|
+
store = service.store_for(request.runtime)
|
|
23
|
+
if store is None:
|
|
24
|
+
raise APIError("Attaching files needs Mobster's data folder. Run Mobster with its task history on.", 503,
|
|
25
|
+
"files_unavailable")
|
|
26
|
+
return store
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _found(store, attachment_id):
|
|
30
|
+
found = store.get(attachment_id)
|
|
31
|
+
if found is None:
|
|
32
|
+
raise APIError("That file isn't here any more. Attach it again.", 404, "attachment_not_found")
|
|
33
|
+
return found
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _refuse(error):
|
|
37
|
+
if isinstance(error, StoreError):
|
|
38
|
+
return Response.error(str(error), error.status, error.code)
|
|
39
|
+
raise error
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _query(request, name):
|
|
43
|
+
values = request.query.get(name) or []
|
|
44
|
+
if len(values) > 1:
|
|
45
|
+
raise ValueError(f"Supply {name} once")
|
|
46
|
+
return values[0] if values else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def upload(request):
|
|
50
|
+
store = _store(request)
|
|
51
|
+
name = (_query(request, "name") or "").strip()
|
|
52
|
+
if len(name) > 1024:
|
|
53
|
+
raise ValueError("That file name is too long")
|
|
54
|
+
thread = _query(request, "thread")
|
|
55
|
+
if thread is not None and not ID.fullmatch(thread):
|
|
56
|
+
raise ValueError("thread must be a conversation id")
|
|
57
|
+
try:
|
|
58
|
+
attachment = store.create(request.body or b"", name or "file", thread_id=thread)
|
|
59
|
+
except StoreError as error:
|
|
60
|
+
return _refuse(error)
|
|
61
|
+
return Response.json({"attachment": attachment.public()}, 201)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def metadata(request):
|
|
65
|
+
store = _store(request)
|
|
66
|
+
return Response.json({"attachment": _found(store, request.params["id"]).public()})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def content(request):
|
|
70
|
+
store = _store(request)
|
|
71
|
+
attachment = _found(store, request.params["id"])
|
|
72
|
+
path = store.path(attachment, "original")
|
|
73
|
+
if path is None:
|
|
74
|
+
raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
|
|
75
|
+
mime = attachment.data.get("mime") or "application/octet-stream"
|
|
76
|
+
if attachment.kind in ("text", "csv"):
|
|
77
|
+
mime = "text/plain; charset=utf-8" # never served as HTML, JSON or a script
|
|
78
|
+
return Response.bytes(path.read_bytes(), mime)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def thumb(request):
|
|
82
|
+
store = _store(request)
|
|
83
|
+
attachment = _found(store, request.params["id"])
|
|
84
|
+
path = store.path(attachment, "thumb")
|
|
85
|
+
if path is None:
|
|
86
|
+
raise APIError("This file has no thumbnail.", 404, "no_thumbnail")
|
|
87
|
+
return Response.bytes(path.read_bytes(), "image/jpeg")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def delete(request):
|
|
91
|
+
store = _store(request)
|
|
92
|
+
if not store.delete(request.params["id"]):
|
|
93
|
+
raise APIError("That file isn't here any more.", 404, "attachment_not_found")
|
|
94
|
+
return Response.json({"deleted": True})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _phone(request, device):
|
|
98
|
+
"""(fleet phone record, PhoneFiles) for ``device`` (None: the default device)."""
|
|
99
|
+
if device is not None and (not isinstance(device, str) or not 0 < len(device) <= 128):
|
|
100
|
+
raise ValueError("device must be a device id or name")
|
|
101
|
+
record = service.device_record(request.runtime, device)
|
|
102
|
+
try:
|
|
103
|
+
return record, service.phone_files(record)
|
|
104
|
+
except PhoneIOError as error:
|
|
105
|
+
raise _phone_error(error) from None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _phone_error(error):
|
|
109
|
+
status = {"needs_cable": 409, "no_file_sharing": 409, "no_device": 409, "locked": 409, "not_found": 404,
|
|
110
|
+
"tools": 503, "usage": 400}.get(error.code, 502)
|
|
111
|
+
message = str(error) + (f" {error.fix}" if error.fix else "")
|
|
112
|
+
return APIError(message, status, error.code)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def phone_list(request):
|
|
116
|
+
from .agent import file_sharing_apps
|
|
117
|
+
record, phone = _phone(request, _query(request, "device"))
|
|
118
|
+
app = _query(request, "app")
|
|
119
|
+
try:
|
|
120
|
+
out = {"device": {"id": record["id"], "name": record["name"], "kind": record["kind"]},
|
|
121
|
+
"apps": file_sharing_apps(phone)}
|
|
122
|
+
if app:
|
|
123
|
+
out["files"] = phone.ls(app)
|
|
124
|
+
except PhoneIOError as error:
|
|
125
|
+
raise _phone_error(error) from None
|
|
126
|
+
return Response.json(out)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def phone_put(request):
|
|
130
|
+
# The user's own click in the Mac app: api_http.APP_ONLY_ROUTES needs its session, so a process holding only the
|
|
131
|
+
# token file can't put files on the phone.
|
|
132
|
+
body = request.body or {}
|
|
133
|
+
unknown = set(body) - {"device", "attachmentId", "destination"}
|
|
134
|
+
if unknown:
|
|
135
|
+
raise ValueError("Unsupported field: " + ", ".join(sorted(unknown)))
|
|
136
|
+
key = request.headers.get("Idempotency-Key")
|
|
137
|
+
if key is not None and not 0 < len(key) <= 200:
|
|
138
|
+
raise ValueError("Idempotency-Key is 1 to 200 characters")
|
|
139
|
+
store = _store(request)
|
|
140
|
+
attachment = _found(store, body.get("attachmentId") if isinstance(body.get("attachmentId"), str) else "")
|
|
141
|
+
destination = body.get("destination")
|
|
142
|
+
if not isinstance(destination, str) or not (destination in ("clipboard", "photos")
|
|
143
|
+
or destination.startswith(DESTINATION_APP)):
|
|
144
|
+
raise ValueError("destination is clipboard, photos or app:<bundle id>")
|
|
145
|
+
device = body.get("device")
|
|
146
|
+
if device is not None and (not isinstance(device, str) or not 0 < len(device) <= 128):
|
|
147
|
+
raise ValueError("device must be a device id or name")
|
|
148
|
+
runtime = request.runtime
|
|
149
|
+
phone = runtime.resolve_device(device)
|
|
150
|
+
key = key or uuid.uuid4().hex
|
|
151
|
+
fingerprint = {"device": phone.id, "attachmentId": attachment.id, "destination": destination}
|
|
152
|
+
earlier = store.copy_result(key, fingerprint)
|
|
153
|
+
if earlier is not None:
|
|
154
|
+
return Response.json(earlier)
|
|
155
|
+
reserved = False
|
|
156
|
+
def before_copy():
|
|
157
|
+
nonlocal reserved
|
|
158
|
+
if reserved:
|
|
159
|
+
return
|
|
160
|
+
previous = store.reserve_copy(key, fingerprint)
|
|
161
|
+
if previous is not None:
|
|
162
|
+
raise _Copied(previous)
|
|
163
|
+
reserved = True
|
|
164
|
+
|
|
165
|
+
from ..journal import JournalError, Lease
|
|
166
|
+
with runtime.lock:
|
|
167
|
+
if getattr(runtime, "closing", False):
|
|
168
|
+
raise APIError("The service is shutting down", 503, "shutting_down")
|
|
169
|
+
if runtime.run_on(phone) is not None:
|
|
170
|
+
raise APIError(f"{phone.name or 'The iPhone'} is busy with a task. Try again when it ends.", 409,
|
|
171
|
+
"device_busy")
|
|
172
|
+
url = (runtime.config.wda_url if getattr(phone, "primary", False) else phone.wda_url) or ""
|
|
173
|
+
if not url:
|
|
174
|
+
raise APIError(f"{phone.name or 'The device'} isn't set up yet.", 409, "no_device")
|
|
175
|
+
try:
|
|
176
|
+
lease = Lease.device(url.rstrip("/"))
|
|
177
|
+
except JournalError:
|
|
178
|
+
raise APIError("Your iPhone is busy. Wait for it to finish, then try again.", 409, "device_busy") from None
|
|
179
|
+
try:
|
|
180
|
+
earlier = store.copy_result(key, fingerprint)
|
|
181
|
+
if earlier is not None:
|
|
182
|
+
return Response.json(earlier)
|
|
183
|
+
if destination == "clipboard":
|
|
184
|
+
result = _put_clipboard(request, store, attachment, phone.id, before_copy)
|
|
185
|
+
elif destination == "photos":
|
|
186
|
+
result = _put_photos(request, store, attachment, phone.id, before_copy)
|
|
187
|
+
else:
|
|
188
|
+
bundle = destination[len(DESTINATION_APP):]
|
|
189
|
+
record, files = _phone(request, phone.id)
|
|
190
|
+
original = store.path(attachment, "original")
|
|
191
|
+
if original is None:
|
|
192
|
+
raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
|
|
193
|
+
try:
|
|
194
|
+
copied = files.put(bundle, original, name=attachment.name, before_copy=before_copy)
|
|
195
|
+
except PhoneIOError as error:
|
|
196
|
+
raise _phone_error(error) from None
|
|
197
|
+
result = {"status": "copied", "name": copied["name"], "path": copied["path"], "destination": destination,
|
|
198
|
+
"device": record["id"]}
|
|
199
|
+
store.finish_copy(key, result)
|
|
200
|
+
return Response.json(result)
|
|
201
|
+
except _Copied as previous:
|
|
202
|
+
return Response.json(previous.result)
|
|
203
|
+
finally:
|
|
204
|
+
lease.close()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _wda(request, device):
|
|
208
|
+
"""(fleet phone, call(method, path, body, timeout)) for a device's WebDriverAgent session, refused while a task runs
|
|
209
|
+
on it."""
|
|
210
|
+
import json
|
|
211
|
+
import urllib.request
|
|
212
|
+
|
|
213
|
+
from ..phone_io import checked_request
|
|
214
|
+
runtime = request.runtime
|
|
215
|
+
phone = runtime.resolve_device(device)
|
|
216
|
+
if runtime.device_busy(phone.id):
|
|
217
|
+
raise APIError(f"{phone.name or 'The iPhone'} is busy with a task. Try again when it ends.", 409,
|
|
218
|
+
"device_busy")
|
|
219
|
+
url = (phone.wda_url or "").rstrip("/")
|
|
220
|
+
if not url:
|
|
221
|
+
raise APIError(f"{phone.name or 'The device'} isn't set up yet.", 409, "no_device")
|
|
222
|
+
|
|
223
|
+
def send(method, path, body, timeout):
|
|
224
|
+
session = phone.wda_session()
|
|
225
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
226
|
+
req = urllib.request.Request(f"{url}/session/{session}{path}", data=data, method=method,
|
|
227
|
+
headers={"Content-Type": "application/json"})
|
|
228
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
229
|
+
return json.load(response)
|
|
230
|
+
return phone, checked_request(send)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _put_clipboard(request, store, attachment, device, before_copy):
|
|
234
|
+
"""A text file onto the phone's clipboard through WebDriverAgent (any device), never while a task runs on it."""
|
|
235
|
+
from ..phone_io.clipboard import set_text
|
|
236
|
+
if attachment.kind not in ("text", "csv"):
|
|
237
|
+
raise APIError("Only a text file goes on the clipboard.", 400, "not_text")
|
|
238
|
+
phone, call = _wda(request, device)
|
|
239
|
+
text = store.whole_text(attachment) # the file itself, never the excerpt the agent reads
|
|
240
|
+
if text is None:
|
|
241
|
+
raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
|
|
242
|
+
try:
|
|
243
|
+
set_text(_copy_call(call, before_copy), text)
|
|
244
|
+
except PhoneIOError as error:
|
|
245
|
+
raise _phone_error(error) from None
|
|
246
|
+
except OSError:
|
|
247
|
+
raise APIError(f"{phone.name or 'The iPhone'} didn't answer. Keep it unlocked and connected, then try again.",
|
|
248
|
+
502, "phone_unreachable") from None
|
|
249
|
+
return {"status": "copied", "name": attachment.name, "path": None, "destination": "clipboard",
|
|
250
|
+
"device": phone.id}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _put_photos(request, store, attachment, device, before_copy):
|
|
254
|
+
"""A picture or video into the phone's Photos library (phone_io.photos.send: Mobster's runner saves it), never while
|
|
255
|
+
a task runs on the phone."""
|
|
256
|
+
from ..phone_io.photos import send
|
|
257
|
+
if attachment.kind != "image":
|
|
258
|
+
raise APIError("Only a photo or a video goes into Photos.", 400, "not_media")
|
|
259
|
+
original = store.path(attachment, "original")
|
|
260
|
+
if original is None:
|
|
261
|
+
raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
|
|
262
|
+
phone, call = _wda(request, device)
|
|
263
|
+
try:
|
|
264
|
+
added = send(call, original, name=phone.name or "the iPhone",
|
|
265
|
+
activate=lambda bundle: call("POST", "/wda/apps/activate", {"bundleId": bundle}, 15),
|
|
266
|
+
before_send=before_copy)
|
|
267
|
+
except PhoneIOError as error:
|
|
268
|
+
status = {"needs_rebuild": 409, "photos_denied": 409, "photos_restricted": 409,
|
|
269
|
+
"photos_not_determined": 409, "usage": 400}.get(error.code, 502)
|
|
270
|
+
raise APIError(str(error) + (f" {error.fix}" if error.fix else ""), status, error.code) from None
|
|
271
|
+
except OSError:
|
|
272
|
+
raise APIError(f"{phone.name or 'The iPhone'} didn't answer. Keep it unlocked and connected, then try again.",
|
|
273
|
+
502, "phone_unreachable") from None
|
|
274
|
+
return {"status": "copied", "name": attachment.name, "path": None, "destination": "photos", "device": phone.id,
|
|
275
|
+
"kind": added["kind"]}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _copy_call(call, before_copy):
|
|
279
|
+
"""Journal intent after local validation and read-only preflight, before the first WDA mutation."""
|
|
280
|
+
def dispatch(method, path, body=None, timeout=15):
|
|
281
|
+
if method != "GET":
|
|
282
|
+
before_copy()
|
|
283
|
+
return call(method, path, body, timeout)
|
|
284
|
+
return dispatch
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def phone_get(request):
|
|
288
|
+
from ..harness_api import post_thread_item
|
|
289
|
+
body = request.body or {}
|
|
290
|
+
unknown = set(body) - {"device", "app", "name", "threadId"}
|
|
291
|
+
if unknown:
|
|
292
|
+
raise ValueError("Unsupported field: " + ", ".join(sorted(unknown)))
|
|
293
|
+
app, name, thread = body.get("app"), body.get("name"), body.get("threadId")
|
|
294
|
+
if not isinstance(app, str) or not isinstance(name, str) or not name:
|
|
295
|
+
raise ValueError("Name the app (its bundle ID) and the file")
|
|
296
|
+
if thread is not None and (not isinstance(thread, str) or not ID.fullmatch(thread)):
|
|
297
|
+
raise ValueError("threadId must be a conversation id")
|
|
298
|
+
store = _store(request)
|
|
299
|
+
record, phone = _phone(request, body.get("device"))
|
|
300
|
+
with tempfile.TemporaryDirectory(prefix="mobster-phone-file-") as folder:
|
|
301
|
+
try:
|
|
302
|
+
saved = phone.get(app, name, Path(folder))
|
|
303
|
+
except PhoneIOError as error:
|
|
304
|
+
raise _phone_error(error) from None
|
|
305
|
+
if saved.stat().st_size > FILE_MAX:
|
|
306
|
+
raise APIError("Mobster attaches files of 25 MB or less.", 413, "too_large")
|
|
307
|
+
try:
|
|
308
|
+
attachment = store.create(saved.read_bytes(), saved.name, thread_id=thread, origin="phone")
|
|
309
|
+
except StoreError as error:
|
|
310
|
+
return _refuse(error)
|
|
311
|
+
if thread:
|
|
312
|
+
post_thread_item(thread, {"kind": "file_saved", "attachment": attachment.ref(), "app": app,
|
|
313
|
+
"device": record["name"]})
|
|
314
|
+
return Response.json({"attachment": attachment.public()}, 201)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
ROUTES = (
|
|
318
|
+
Route("files", "POST", "/api/attachments", upload, body="raw", max_bytes=FILE_MAX),
|
|
319
|
+
Route("files", "GET", "/api/attachments/{id}", metadata, body="none"),
|
|
320
|
+
Route("files", "GET", "/api/attachments/{id}/content", content, body="none"),
|
|
321
|
+
Route("files", "GET", "/api/attachments/{id}/thumb", thumb, body="none"),
|
|
322
|
+
Route("files", "DELETE", "/api/attachments/{id}", delete, body="none"),
|
|
323
|
+
Route("files", "GET", "/api/phone/files", phone_list, body="none"),
|
|
324
|
+
Route("files", "POST", "/api/phone/files/put", phone_put, idempotency=True),
|
|
325
|
+
Route("files", "POST", "/api/phone/files/get", phone_get),
|
|
326
|
+
)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""The attachments service per Runtime: its store, the run listener that records which task used which file, and the
|
|
2
|
+
clean-up thread."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import weakref
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger("mobster.attachments")
|
|
10
|
+
|
|
11
|
+
GC_FIRST, GC_EVERY, BUS_WAIT = 60.0, 1800.0, 5.0
|
|
12
|
+
_stores = weakref.WeakKeyDictionary()
|
|
13
|
+
_lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def store_for(runtime):
|
|
17
|
+
"""The runtime's AttachmentStore, or None when it keeps no journal on disk."""
|
|
18
|
+
from .store import AttachmentStore
|
|
19
|
+
with _lock:
|
|
20
|
+
if getattr(runtime, "closing", False):
|
|
21
|
+
return None
|
|
22
|
+
found = _stores.get(runtime)
|
|
23
|
+
if found is not None:
|
|
24
|
+
return found
|
|
25
|
+
state_db = getattr(getattr(runtime, "config", None), "state_db", None)
|
|
26
|
+
journal = getattr(runtime, "journal", None)
|
|
27
|
+
if not state_db or journal is None:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def publish(event):
|
|
31
|
+
post = getattr(runtime, "publish", None)
|
|
32
|
+
if callable(post):
|
|
33
|
+
post("attachments", event)
|
|
34
|
+
|
|
35
|
+
store = AttachmentStore(journal, Path(state_db).parent / "attachments", publish=publish,
|
|
36
|
+
referenced=lambda: referenced(runtime))
|
|
37
|
+
try:
|
|
38
|
+
# Called under the runtime's lock (delete_run, create's retention): only note the ids here; the
|
|
39
|
+
# clean-up thread deletes what they leave unused.
|
|
40
|
+
journal.add_expiry_listener(lambda run_ids: expire_later(runtime, run_ids))
|
|
41
|
+
except Exception: # noqa: BLE001
|
|
42
|
+
log.exception("could not listen for expired tasks")
|
|
43
|
+
_stores[runtime] = store
|
|
44
|
+
return store
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def device_record(runtime, device=None):
|
|
48
|
+
"""A devices.py-shaped record of the device ``device`` names (an id, UDID or name; None: the default device), for
|
|
49
|
+
phone_io.files."""
|
|
50
|
+
phone = runtime.resolve_device(device)
|
|
51
|
+
return {"id": phone.id, "kind": phone.kind, "udid": phone.udid, "name": phone.name or phone.id,
|
|
52
|
+
"transport": getattr(phone, "transport", None), "wdaUrl": phone.wda_url}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def phone_files(record):
|
|
56
|
+
"""phone_io.files.PhoneFiles for ``record`` (tests replace this)."""
|
|
57
|
+
from ..phone_io.files import PhoneFiles
|
|
58
|
+
return PhoneFiles(record)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def referenced(runtime):
|
|
62
|
+
"""{attachment id: [run ids]} the tasks in memory name in their ``attachmentIds``."""
|
|
63
|
+
out = {}
|
|
64
|
+
lock = getattr(runtime, "lock", None)
|
|
65
|
+
if lock is None:
|
|
66
|
+
return out
|
|
67
|
+
with lock:
|
|
68
|
+
runs = list(getattr(runtime, "runs", {}).values())
|
|
69
|
+
for run in runs:
|
|
70
|
+
for identifier in (getattr(run, "extras", None) or {}).get("attachmentIds") or ():
|
|
71
|
+
out.setdefault(identifier, []).append(run.id)
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def expire_later(runtime, run_ids):
|
|
76
|
+
"""The journal's expiry listener: hand the removed tasks to the clean-up thread (there is one whenever this runtime
|
|
77
|
+
keeps any file)."""
|
|
78
|
+
cleaner = _cleaners.get(runtime)
|
|
79
|
+
if cleaner is not None and run_ids:
|
|
80
|
+
with cleaner.lock:
|
|
81
|
+
cleaner.expired.update(run_ids)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Cleaner:
|
|
85
|
+
def __init__(self, runtime):
|
|
86
|
+
self.runtime = runtime
|
|
87
|
+
self.stop = threading.Event()
|
|
88
|
+
self.lock = threading.Lock()
|
|
89
|
+
self.expired = set()
|
|
90
|
+
self.thread = threading.Thread(target=self._loop, name="mobster-attachments", daemon=True)
|
|
91
|
+
|
|
92
|
+
def take_expired(self):
|
|
93
|
+
with self.lock:
|
|
94
|
+
ids, self.expired = self.expired, set()
|
|
95
|
+
return ids
|
|
96
|
+
|
|
97
|
+
def _loop(self):
|
|
98
|
+
import time
|
|
99
|
+
|
|
100
|
+
from ..eventbus import bus as default_bus
|
|
101
|
+
bus = getattr(self.runtime, "bus", None) or default_bus
|
|
102
|
+
cursor = bus.position()
|
|
103
|
+
due = time.monotonic() + GC_FIRST
|
|
104
|
+
while not self.stop.is_set():
|
|
105
|
+
try:
|
|
106
|
+
events = bus.wait({"threads"}, cursor, BUS_WAIT)
|
|
107
|
+
except Exception: # noqa: BLE001
|
|
108
|
+
events = []
|
|
109
|
+
self.stop.wait(BUS_WAIT)
|
|
110
|
+
if events and isinstance(events[-1].get("id"), str):
|
|
111
|
+
cursor = events[-1]["id"]
|
|
112
|
+
store = store_for(self.runtime) if not self.stop.is_set() else None
|
|
113
|
+
if store is None:
|
|
114
|
+
continue
|
|
115
|
+
expired = self.take_expired()
|
|
116
|
+
if expired:
|
|
117
|
+
try:
|
|
118
|
+
store.expired(sorted(expired))
|
|
119
|
+
except Exception: # noqa: BLE001
|
|
120
|
+
log.exception("could not clean up the files of tasks that left history")
|
|
121
|
+
for event in events:
|
|
122
|
+
thread = event.get("thread") if isinstance(event.get("thread"), dict) else {}
|
|
123
|
+
if event.get("event") == "thread_deleted" and thread.get("id"):
|
|
124
|
+
try:
|
|
125
|
+
store.delete_thread(thread["id"])
|
|
126
|
+
except Exception: # noqa: BLE001
|
|
127
|
+
log.exception("could not delete a conversation's files")
|
|
128
|
+
if time.monotonic() >= due:
|
|
129
|
+
due = time.monotonic() + GC_EVERY
|
|
130
|
+
try:
|
|
131
|
+
store.gc()
|
|
132
|
+
except Exception: # noqa: BLE001
|
|
133
|
+
log.exception("attachment clean-up failed")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_cleaners = weakref.WeakKeyDictionary()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def start(runtime):
|
|
140
|
+
"""register_service start: the store, and its clean-up thread once there is anything to clean (now, or at the first
|
|
141
|
+
upload)."""
|
|
142
|
+
store = store_for(runtime)
|
|
143
|
+
if store is None:
|
|
144
|
+
return
|
|
145
|
+
store.on_first_write = lambda: ensure_cleaner(runtime)
|
|
146
|
+
try:
|
|
147
|
+
kept = store.count()
|
|
148
|
+
except Exception: # noqa: BLE001 -- a journal that can't answer starts no thread; the first upload will
|
|
149
|
+
kept = 0
|
|
150
|
+
if kept:
|
|
151
|
+
ensure_cleaner(runtime)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def ensure_cleaner(runtime):
|
|
155
|
+
with _lock:
|
|
156
|
+
if runtime in _cleaners or getattr(runtime, "closing", False):
|
|
157
|
+
return
|
|
158
|
+
cleaner = Cleaner(runtime)
|
|
159
|
+
_cleaners[runtime] = cleaner
|
|
160
|
+
cleaner.thread.start()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def close(runtime):
|
|
164
|
+
with _lock:
|
|
165
|
+
store = _stores.pop(runtime, None)
|
|
166
|
+
cleaner = _cleaners.pop(runtime, None)
|
|
167
|
+
if store is not None:
|
|
168
|
+
store.on_first_write = None
|
|
169
|
+
if cleaner is not None:
|
|
170
|
+
cleaner.stop.set()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""What a file is, from its first bytes, never from its name: the attachment allowlist."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
KINDS = ("image", "pdf", "text", "csv")
|
|
10
|
+
NAME_LIMIT = 120
|
|
11
|
+
# Characters a name never keeps: C0/C1 controls, and the invisible marks that make a name read differently.
|
|
12
|
+
_UNSAFE = re.compile(r"[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]")
|
|
13
|
+
_HEIF_BRANDS = {b"heic", b"heix", b"hevc", b"hevx", b"heim", b"heis", b"hevm", b"hevs", b"mif1", b"msf1", b"avif"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Unsupported(ValueError):
|
|
17
|
+
"""The file isn't one Mobster reads. ``code``: unsupported_type."""
|
|
18
|
+
|
|
19
|
+
code = "unsupported_type"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class FileType:
|
|
24
|
+
mime: str
|
|
25
|
+
kind: str # image | pdf | text | csv
|
|
26
|
+
ext: str # the extension Mobster stores it under (no dot)
|
|
27
|
+
label: str # a word for people: "PDF", "PNG image", "CSV"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
IMAGE_TYPES = {
|
|
31
|
+
"image/png": FileType("image/png", "image", "png", "PNG image"),
|
|
32
|
+
"image/jpeg": FileType("image/jpeg", "image", "jpg", "JPEG image"),
|
|
33
|
+
"image/gif": FileType("image/gif", "image", "gif", "GIF image"),
|
|
34
|
+
"image/webp": FileType("image/webp", "image", "webp", "WebP image"),
|
|
35
|
+
"image/heic": FileType("image/heic", "image", "heic", "HEIC photo"),
|
|
36
|
+
}
|
|
37
|
+
PDF = FileType("application/pdf", "pdf", "pdf", "PDF")
|
|
38
|
+
PLAIN = FileType("text/plain", "text", "txt", "Text")
|
|
39
|
+
MARKDOWN = FileType("text/markdown", "text", "md", "Markdown")
|
|
40
|
+
JSON_TYPE = FileType("application/json", "text", "json", "JSON")
|
|
41
|
+
CSV = FileType("text/csv", "csv", "csv", "CSV")
|
|
42
|
+
TSV = FileType("text/tab-separated-values", "csv", "tsv", "TSV")
|
|
43
|
+
|
|
44
|
+
UNSUPPORTED = "Mobster reads images, PDFs, text and CSV files. This file is none of those."
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def image_type(head):
|
|
48
|
+
"""The image type ``head`` (a file's first bytes) starts with, or None."""
|
|
49
|
+
if head.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
50
|
+
return IMAGE_TYPES["image/png"]
|
|
51
|
+
if head.startswith(b"\xff\xd8\xff"):
|
|
52
|
+
return IMAGE_TYPES["image/jpeg"]
|
|
53
|
+
if head.startswith((b"GIF87a", b"GIF89a")):
|
|
54
|
+
return IMAGE_TYPES["image/gif"]
|
|
55
|
+
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
|
56
|
+
return IMAGE_TYPES["image/webp"]
|
|
57
|
+
if head[4:8] == b"ftyp" and head[8:12] in _HEIF_BRANDS:
|
|
58
|
+
return IMAGE_TYPES["image/heic"]
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_pdf(head):
|
|
63
|
+
"""A PDF's header ``%PDF-`` within its first 1 KB (some writers put junk before it, as readers allow)."""
|
|
64
|
+
return b"%PDF-" in head[:1024]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def decode_text(data):
|
|
68
|
+
"""``data`` as text, or None when it isn't UTF-8 text: it must decode, hold no NUL, and at most 1 in 1,000 of its
|
|
69
|
+
characters may be a control character other than tab, newline, carriage return or form feed."""
|
|
70
|
+
if b"\x00" in data:
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
text = data.decode("utf-8")
|
|
74
|
+
except UnicodeDecodeError:
|
|
75
|
+
return None
|
|
76
|
+
if text.startswith("\ufeff"):
|
|
77
|
+
text = text[1:]
|
|
78
|
+
controls = sum(1 for ch in text if (ord(ch) < 32 and ch not in "\t\n\r\f") or ord(ch) == 127)
|
|
79
|
+
if controls > max(1, len(text) // 1000):
|
|
80
|
+
return None
|
|
81
|
+
return text
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def sniff(data, name=""):
|
|
85
|
+
"""The FileType of ``data`` (the whole file), or Unsupported. The name only says which text a text file is."""
|
|
86
|
+
head = bytes(data[:4096])
|
|
87
|
+
found = image_type(head)
|
|
88
|
+
if found is not None:
|
|
89
|
+
return found
|
|
90
|
+
if is_pdf(head):
|
|
91
|
+
return PDF
|
|
92
|
+
text = decode_text(bytes(data))
|
|
93
|
+
if text is None:
|
|
94
|
+
raise Unsupported(UNSUPPORTED)
|
|
95
|
+
ext = os.path.splitext(str(name or "").lower())[1]
|
|
96
|
+
if ext == ".csv":
|
|
97
|
+
return CSV
|
|
98
|
+
if ext == ".tsv":
|
|
99
|
+
return TSV
|
|
100
|
+
if ext == ".json":
|
|
101
|
+
try:
|
|
102
|
+
json.loads(text)
|
|
103
|
+
return JSON_TYPE
|
|
104
|
+
except ValueError:
|
|
105
|
+
return PLAIN
|
|
106
|
+
if ext in (".md", ".markdown"):
|
|
107
|
+
return MARKDOWN
|
|
108
|
+
return PLAIN
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def clean_name(name, fallback="file"):
|
|
112
|
+
"""A file name that is safe to show and to write anywhere: the last path part only, Unicode NFC, without control or
|
|
113
|
+
direction-override characters, spaces collapsed, never "", "." or "..", never starting with "-" or ".", at most
|
|
114
|
+
NAME_LIMIT characters with its extension kept."""
|
|
115
|
+
text = unicodedata.normalize("NFC", str(name or ""))
|
|
116
|
+
text = text.replace("\\", "/").rsplit("/", 1)[-1]
|
|
117
|
+
text = _UNSAFE.sub("", text)
|
|
118
|
+
text = " ".join(text.split()).strip(" .")
|
|
119
|
+
text = text.lstrip("-").strip()
|
|
120
|
+
if not text or text in (".", ".."):
|
|
121
|
+
text = fallback
|
|
122
|
+
if len(text) > NAME_LIMIT:
|
|
123
|
+
stem, ext = os.path.splitext(text)
|
|
124
|
+
ext = ext if len(ext) <= 12 else ""
|
|
125
|
+
text = stem[:NAME_LIMIT - len(ext)].rstrip(" .") + ext
|
|
126
|
+
return text
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def with_extension(name, file_type):
|
|
130
|
+
"""``name`` ending in an extension that matches ``file_type`` (``scan`` + PDF -> ``scan.pdf``); a name that already
|
|
131
|
+
has a fitting one keeps it (``photo.jpeg`` stays)."""
|
|
132
|
+
stem, ext = os.path.splitext(name)
|
|
133
|
+
ext = ext.lower().lstrip(".")
|
|
134
|
+
fitting = {"jpg": {"jpg", "jpeg"}, "heic": {"heic", "heif"}, "txt": {"txt", "text", "log", ""},
|
|
135
|
+
"md": {"md", "markdown"}, "csv": {"csv"}, "tsv": {"tsv"}, "json": {"json"}}
|
|
136
|
+
if ext == file_type.ext or ext in fitting.get(file_type.ext, set()):
|
|
137
|
+
return name if ext or file_type.kind not in ("image", "pdf") else f"{name}.{file_type.ext}"
|
|
138
|
+
if file_type.kind == "text" and file_type.ext == "txt":
|
|
139
|
+
return name # a .log or .yaml file keeps its name; it is read as plain text
|
|
140
|
+
return clean_name(f"{name}.{file_type.ext}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def size_words(count):
|
|
144
|
+
"""``412 KB``, ``3.4 MB``: a file's size for people (1 KB = 1,000 bytes, as Finder counts)."""
|
|
145
|
+
count = int(count or 0)
|
|
146
|
+
if count < 1000:
|
|
147
|
+
return f"{count} byte" + ("" if count == 1 else "s")
|
|
148
|
+
for unit, step in (("KB", 1e3), ("MB", 1e6), ("GB", 1e9)):
|
|
149
|
+
value = count / step
|
|
150
|
+
if value < 999.5 or unit == "GB":
|
|
151
|
+
return f"{value:.0f} {unit}" if value >= 10 or unit == "KB" else f"{value:.1f} {unit}"
|
|
152
|
+
return f"{count} bytes"
|