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,441 @@
|
|
|
1
|
+
"""Attachments on this Mac: the journal's ``attachments`` tables and the files beside the journal."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import extract, sniff
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("mobster.attachments")
|
|
18
|
+
|
|
19
|
+
SCHEMA, VERSION = "attachments", 2
|
|
20
|
+
FILE_MAX = 26_214_400 # 25 MB, the upload route's cap
|
|
21
|
+
THREAD_MAX = 100 * 1024 * 1024
|
|
22
|
+
TOTAL_MAX = 1024 * 1024 * 1024
|
|
23
|
+
PER_RUN = 10
|
|
24
|
+
UNUSED_TTL = 24 * 3600
|
|
25
|
+
ORPHAN_TTL = 3600
|
|
26
|
+
COPY_TTL = 600
|
|
27
|
+
COPY_LIMIT = 10_000
|
|
28
|
+
ID = re.compile(r"[a-f0-9]{12}")
|
|
29
|
+
ORIGINS = ("upload", "phone", "screenshot")
|
|
30
|
+
DERIVED = {"thumb": "thumb.jpg", "model": "model.jpg", "text": "text.txt"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def migrate(connection, from_version):
|
|
34
|
+
"""The attachments schema: v1 keeps files and run references; v2 journals keyed phone copies."""
|
|
35
|
+
if from_version < 1:
|
|
36
|
+
connection.execute("CREATE TABLE IF NOT EXISTS attachments(id TEXT PRIMARY KEY, thread_id TEXT, run_id TEXT, "
|
|
37
|
+
"data TEXT NOT NULL, bytes INTEGER NOT NULL, created_at REAL NOT NULL)")
|
|
38
|
+
connection.execute("CREATE INDEX IF NOT EXISTS attachments_thread ON attachments(thread_id)")
|
|
39
|
+
connection.execute("CREATE TABLE IF NOT EXISTS attachment_runs("
|
|
40
|
+
"attachment_id TEXT NOT NULL REFERENCES attachments(id) ON DELETE CASCADE, "
|
|
41
|
+
"run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, "
|
|
42
|
+
"PRIMARY KEY(attachment_id, run_id))")
|
|
43
|
+
connection.execute("CREATE INDEX IF NOT EXISTS attachment_runs_run ON attachment_runs(run_id)")
|
|
44
|
+
if from_version < 2:
|
|
45
|
+
connection.execute("CREATE TABLE IF NOT EXISTS attachment_copies("
|
|
46
|
+
"key TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, result TEXT, created_at REAL NOT NULL)")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StoreError(ValueError):
|
|
50
|
+
"""A refusal with a sentence, an HTTP status and a code (too_large, thread_full, storage_full, ...)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message, status=400, code="invalid_attachment"):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.status, self.code = status, code
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Attachment:
|
|
59
|
+
id: str
|
|
60
|
+
thread_id: object
|
|
61
|
+
run_id: object
|
|
62
|
+
data: dict
|
|
63
|
+
bytes: int
|
|
64
|
+
created_at: float
|
|
65
|
+
run_ids: tuple = ()
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def name(self):
|
|
69
|
+
return self.data.get("name") or "file"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def kind(self):
|
|
73
|
+
return self.data.get("kind")
|
|
74
|
+
|
|
75
|
+
def ref(self):
|
|
76
|
+
"""The AttachmentRef every surface shares: {id, name, mime, bytes, kind}."""
|
|
77
|
+
return {"id": self.id, "name": self.name, "mime": self.data.get("mime"), "bytes": self.data.get("bytes"),
|
|
78
|
+
"kind": self.kind}
|
|
79
|
+
|
|
80
|
+
def public(self):
|
|
81
|
+
"""The API's attachment: the ref, plus pages or rows, where its thumbnail and content are,
|
|
82
|
+
and where it belongs."""
|
|
83
|
+
out = self.ref()
|
|
84
|
+
for key in ("pages", "rows", "columns", "width", "height"):
|
|
85
|
+
if self.data.get(key) is not None:
|
|
86
|
+
out[key] = self.data[key]
|
|
87
|
+
out.update(label=self.data.get("label"), origin=self.data.get("origin", "upload"),
|
|
88
|
+
textAvailable=bool(self.data.get("textAvailable")),
|
|
89
|
+
thumbUrl=f"/api/attachments/{self.id}/thumb" if self.data.get("thumb") else None,
|
|
90
|
+
contentUrl=f"/api/attachments/{self.id}/content", createdAt=round(self.created_at * 1000),
|
|
91
|
+
threadId=self.thread_id, runIds=list(self.run_ids))
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def new_id():
|
|
96
|
+
return uuid.uuid4().hex[:12]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AttachmentStore:
|
|
100
|
+
"""One journal's attachments."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, journal, root, publish=None, clock=time.time, referenced=None):
|
|
103
|
+
self.journal, self.root = journal, Path(root)
|
|
104
|
+
self.publish = publish or (lambda event: None)
|
|
105
|
+
self.clock = clock
|
|
106
|
+
# {attachment id: [run ids]} of the tasks in memory that name it (service.referenced); kept by clean-up.
|
|
107
|
+
self.referenced = referenced or (lambda: {})
|
|
108
|
+
self._lock = threading.Lock()
|
|
109
|
+
self.on_first_write = None # service.start: the clean-up thread starts with the first file kept
|
|
110
|
+
|
|
111
|
+
def count(self):
|
|
112
|
+
with self.journal.transaction() as connection:
|
|
113
|
+
return connection.execute("SELECT count(*) FROM attachments").fetchone()[0]
|
|
114
|
+
|
|
115
|
+
def _row(self, connection, row):
|
|
116
|
+
if row is None:
|
|
117
|
+
return None
|
|
118
|
+
identifier, thread_id, run_id, data, size, created = row
|
|
119
|
+
runs = tuple(r[0] for r in connection.execute(
|
|
120
|
+
"SELECT run_id FROM attachment_runs WHERE attachment_id=? ORDER BY rowid", (identifier,)))
|
|
121
|
+
try:
|
|
122
|
+
parsed = json.loads(data)
|
|
123
|
+
except ValueError:
|
|
124
|
+
parsed = {}
|
|
125
|
+
return Attachment(identifier, thread_id, run_id, parsed if isinstance(parsed, dict) else {}, size, created,
|
|
126
|
+
runs)
|
|
127
|
+
|
|
128
|
+
def get(self, attachment_id, runs=True):
|
|
129
|
+
"""The attachment, with ``run_ids``: the tasks that read it, then (``runs``) any other task in memory that names
|
|
130
|
+
it. Never call it with ``runs`` while holding the runtime's lock."""
|
|
131
|
+
if not isinstance(attachment_id, str) or not ID.fullmatch(attachment_id):
|
|
132
|
+
return None
|
|
133
|
+
with self.journal.transaction() as connection:
|
|
134
|
+
row = connection.execute("SELECT id, thread_id, run_id, data, bytes, created_at FROM attachments "
|
|
135
|
+
"WHERE id=?", (attachment_id,)).fetchone()
|
|
136
|
+
found = self._row(connection, row)
|
|
137
|
+
if found is not None and runs:
|
|
138
|
+
named = [r for r in self._referenced().get(found.id, ()) if r not in found.run_ids]
|
|
139
|
+
found.run_ids = found.run_ids + tuple(named)
|
|
140
|
+
return found
|
|
141
|
+
|
|
142
|
+
def _referenced(self):
|
|
143
|
+
try:
|
|
144
|
+
return dict(self.referenced() or {})
|
|
145
|
+
except Exception: # noqa: BLE001
|
|
146
|
+
log.exception("could not read which tasks name attachments")
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
def many(self, ids):
|
|
150
|
+
"""The attachments ``ids`` names that exist, in that order."""
|
|
151
|
+
out = []
|
|
152
|
+
for identifier in ids or ():
|
|
153
|
+
found = self.get(identifier, runs=False)
|
|
154
|
+
if found is not None:
|
|
155
|
+
out.append(found)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
def folder(self, attachment_id):
|
|
159
|
+
if not ID.fullmatch(str(attachment_id)):
|
|
160
|
+
raise ValueError("Not an attachment id")
|
|
161
|
+
return self.root / attachment_id
|
|
162
|
+
|
|
163
|
+
def path(self, attachment, which):
|
|
164
|
+
"""The file ``which`` ("original", "thumb", "model", "text") of ``attachment``, or None when it has none."""
|
|
165
|
+
folder = self.folder(attachment.id)
|
|
166
|
+
if which == "original":
|
|
167
|
+
path = folder / f"original.{attachment.data.get('ext') or 'bin'}"
|
|
168
|
+
else:
|
|
169
|
+
path = folder / DERIVED[which]
|
|
170
|
+
if path.is_symlink() or not path.is_file():
|
|
171
|
+
return None
|
|
172
|
+
return path
|
|
173
|
+
|
|
174
|
+
def text(self, attachment):
|
|
175
|
+
"""What Mobster read from the file (``text.txt``): for a CSV only its header and first 200 rows."""
|
|
176
|
+
path = self.path(attachment, "text")
|
|
177
|
+
if path is None:
|
|
178
|
+
return ""
|
|
179
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
180
|
+
|
|
181
|
+
def whole_text(self, attachment):
|
|
182
|
+
"""A text or CSV file's own text, all of it, for the clipboard: never the excerpt the agent reads, so what
|
|
183
|
+
lands on the phone is the file the person approved. UTF-8 without its byte-order mark, line ends as "\\n".
|
|
184
|
+
None when the file is gone or isn't text."""
|
|
185
|
+
if attachment.kind not in ("text", "csv"):
|
|
186
|
+
return None
|
|
187
|
+
path = self.path(attachment, "original")
|
|
188
|
+
if path is None:
|
|
189
|
+
return None
|
|
190
|
+
text = sniff.decode_text(path.read_bytes())
|
|
191
|
+
if text is None:
|
|
192
|
+
return None
|
|
193
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
194
|
+
|
|
195
|
+
def reserve_copy(self, key, fingerprint):
|
|
196
|
+
"""Reserve a keyed copy before dispatch, or return its acknowledged result. An unresolved intent never expires:
|
|
197
|
+
the copy may already be on the phone."""
|
|
198
|
+
from ..api_errors import APIError
|
|
199
|
+
now = self.clock()
|
|
200
|
+
fingerprint = json.dumps(fingerprint, sort_keys=True, separators=(",", ":"))
|
|
201
|
+
refusal = None
|
|
202
|
+
with self.journal.transaction() as connection:
|
|
203
|
+
connection.execute("DELETE FROM attachment_copies WHERE result IS NOT NULL AND created_at < ?",
|
|
204
|
+
(now - COPY_TTL,))
|
|
205
|
+
row = connection.execute("SELECT fingerprint, result FROM attachment_copies WHERE key=?", (key,)).fetchone()
|
|
206
|
+
if row is None and connection.execute("SELECT count(*) FROM attachment_copies").fetchone()[0] >= COPY_LIMIT:
|
|
207
|
+
refusal = APIError("Mobster's copy journal is full. Check unresolved copies on your iPhone.",
|
|
208
|
+
503, "copy_journal_full")
|
|
209
|
+
elif row is None:
|
|
210
|
+
connection.execute("INSERT INTO attachment_copies(key, fingerprint, created_at) VALUES (?, ?, ?)",
|
|
211
|
+
(key, fingerprint, now))
|
|
212
|
+
if refusal is not None:
|
|
213
|
+
raise refusal
|
|
214
|
+
return self._copy_result(row, fingerprint)
|
|
215
|
+
|
|
216
|
+
def copy_result(self, key, fingerprint):
|
|
217
|
+
"""A previous copy's result, or None before dispatch. Never takes the device's lease."""
|
|
218
|
+
fingerprint = json.dumps(fingerprint, sort_keys=True, separators=(",", ":"))
|
|
219
|
+
with self.journal.transaction() as connection:
|
|
220
|
+
row = (connection.execute("SELECT fingerprint, result FROM attachment_copies WHERE key=? "
|
|
221
|
+
"AND (result IS NULL OR created_at >= ?)",
|
|
222
|
+
(key, self.clock() - COPY_TTL)).fetchone())
|
|
223
|
+
return self._copy_result(row, fingerprint)
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _copy_result(row, fingerprint):
|
|
227
|
+
from ..api_errors import APIError
|
|
228
|
+
if row is None:
|
|
229
|
+
return None
|
|
230
|
+
if row[0] != fingerprint:
|
|
231
|
+
raise APIError("This Idempotency-Key was used for another copy.", 409, "idempotency_conflict")
|
|
232
|
+
if row[1] is None:
|
|
233
|
+
raise APIError("This copy already started, and Mobster can't confirm its outcome. "
|
|
234
|
+
"Check the destination on your iPhone before starting another copy.",
|
|
235
|
+
409, "copy_outcome_unknown")
|
|
236
|
+
return json.loads(row[1])
|
|
237
|
+
|
|
238
|
+
def finish_copy(self, key, result):
|
|
239
|
+
"""Acknowledge a reserved copy only after the phone operation returns."""
|
|
240
|
+
with self.journal.transaction() as connection:
|
|
241
|
+
connection.execute("UPDATE attachment_copies SET result=?, created_at=? WHERE key=?",
|
|
242
|
+
(json.dumps(result, separators=(",", ":")), self.clock(), key))
|
|
243
|
+
|
|
244
|
+
def usage(self, thread_id=None):
|
|
245
|
+
"""(bytes kept in all, bytes kept for ``thread_id``)."""
|
|
246
|
+
with self.journal.transaction() as connection:
|
|
247
|
+
return self._usage(connection, thread_id)
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _usage(connection, thread_id):
|
|
251
|
+
total = connection.execute("SELECT COALESCE(SUM(bytes), 0) FROM attachments").fetchone()[0]
|
|
252
|
+
thread = 0
|
|
253
|
+
if thread_id:
|
|
254
|
+
thread = connection.execute("SELECT COALESCE(SUM(bytes), 0) FROM attachments WHERE thread_id=?",
|
|
255
|
+
(thread_id,)).fetchone()[0]
|
|
256
|
+
return int(total), int(thread)
|
|
257
|
+
|
|
258
|
+
def _make_root(self):
|
|
259
|
+
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
260
|
+
if self.root.is_symlink():
|
|
261
|
+
raise StoreError("Mobster's attachments folder is a link; it won't write there.", 500, "io_error")
|
|
262
|
+
os.chmod(self.root, 0o700)
|
|
263
|
+
|
|
264
|
+
def create(self, data, name, *, thread_id=None, origin="upload"):
|
|
265
|
+
"""Keep ``data`` (a whole file) as an attachment named ``name``."""
|
|
266
|
+
data = bytes(data or b"")
|
|
267
|
+
if not data:
|
|
268
|
+
raise StoreError("This file is empty.", 400, "empty_file")
|
|
269
|
+
if len(data) > FILE_MAX:
|
|
270
|
+
raise StoreError("Attach files of 25 MB or less.", 413, "too_large")
|
|
271
|
+
if thread_id is not None and not ID.fullmatch(str(thread_id)):
|
|
272
|
+
raise StoreError("Not a conversation id.", 400, "invalid_thread")
|
|
273
|
+
if origin not in ORIGINS:
|
|
274
|
+
raise ValueError("Unknown attachment origin")
|
|
275
|
+
try:
|
|
276
|
+
file_type = sniff.sniff(data, name)
|
|
277
|
+
except sniff.Unsupported as error:
|
|
278
|
+
raise StoreError(str(error), 415, error.code) from None
|
|
279
|
+
display = sniff.with_extension(sniff.clean_name(name, fallback=f"file.{file_type.ext}"), file_type)
|
|
280
|
+
total, thread = self.usage(thread_id)
|
|
281
|
+
self._check_room(len(data), total, thread)
|
|
282
|
+
self._make_root()
|
|
283
|
+
identifier = new_id()
|
|
284
|
+
folder = self.folder(identifier)
|
|
285
|
+
folder.mkdir(mode=0o700)
|
|
286
|
+
try:
|
|
287
|
+
original = folder / f"original.{file_type.ext}"
|
|
288
|
+
fd = os.open(original, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
289
|
+
with os.fdopen(fd, "wb") as handle:
|
|
290
|
+
handle.write(data)
|
|
291
|
+
try:
|
|
292
|
+
derived = extract.derive(folder, original, file_type)
|
|
293
|
+
except extract.TooLarge as error:
|
|
294
|
+
raise StoreError(str(error), 413, error.code) from None
|
|
295
|
+
except extract.Locked as error:
|
|
296
|
+
raise StoreError(str(error), 415, error.code) from None
|
|
297
|
+
except sniff.Unsupported as error:
|
|
298
|
+
raise StoreError(str(error), 415, error.code) from None
|
|
299
|
+
for path in folder.iterdir(): # the helper's files too: private to this user
|
|
300
|
+
if path.is_file() and not path.is_symlink():
|
|
301
|
+
os.chmod(path, 0o600)
|
|
302
|
+
size = sum(path.stat().st_size for path in folder.iterdir() if path.is_file())
|
|
303
|
+
record = {"id": identifier, "name": display, "mime": file_type.mime, "kind": file_type.kind,
|
|
304
|
+
"ext": file_type.ext, "label": file_type.label, "bytes": len(data),
|
|
305
|
+
"sha256": hashlib.sha256(data).hexdigest(), "origin": origin, **derived}
|
|
306
|
+
created = self.clock()
|
|
307
|
+
# A refusal is raised after the transaction: journal.transaction turns anything raised inside it into
|
|
308
|
+
# a JournalError.
|
|
309
|
+
room = None
|
|
310
|
+
with self._lock, self.journal.transaction() as connection:
|
|
311
|
+
total, thread = self._usage(connection, thread_id)
|
|
312
|
+
room = self._room(size, total, thread)
|
|
313
|
+
if room is None:
|
|
314
|
+
connection.execute("INSERT INTO attachments(id, thread_id, run_id, data, bytes, created_at) "
|
|
315
|
+
"VALUES (?, ?, NULL, ?, ?, ?)",
|
|
316
|
+
(identifier, thread_id, json.dumps(record, separators=(",", ":")), size,
|
|
317
|
+
created))
|
|
318
|
+
if room is not None:
|
|
319
|
+
raise room
|
|
320
|
+
except BaseException:
|
|
321
|
+
shutil.rmtree(folder, ignore_errors=True)
|
|
322
|
+
raise
|
|
323
|
+
self.publish({"event": "attachment_created", "id": identifier, "threadId": thread_id})
|
|
324
|
+
if self.on_first_write is not None:
|
|
325
|
+
hook, self.on_first_write = self.on_first_write, None
|
|
326
|
+
try:
|
|
327
|
+
hook()
|
|
328
|
+
except Exception: # noqa: BLE001
|
|
329
|
+
log.exception("the clean-up couldn't start")
|
|
330
|
+
return Attachment(identifier, thread_id, None, record, size, created, ())
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def _check_room(cls, size, total, thread):
|
|
334
|
+
refusal = cls._room(size, total, thread)
|
|
335
|
+
if refusal is not None:
|
|
336
|
+
raise refusal
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _room(size, total, thread):
|
|
340
|
+
"""None when ``size`` more bytes fit, else the StoreError that says why not."""
|
|
341
|
+
if total + size > TOTAL_MAX:
|
|
342
|
+
return StoreError("Mobster keeps at most 1 GB of attached files. Delete conversations you're done with, "
|
|
343
|
+
"then attach this again.", 413, "storage_full")
|
|
344
|
+
if thread + size > THREAD_MAX:
|
|
345
|
+
return StoreError("This conversation already holds 100 MB of files. Start a new one to attach more.",
|
|
346
|
+
413, "thread_full")
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def attach(self, ids, run_id, thread_id=None):
|
|
350
|
+
"""Record that the task ``run_id`` used ``ids``; files not yet in a conversation join ``thread_id``."""
|
|
351
|
+
recorded = []
|
|
352
|
+
with self.journal.transaction() as connection:
|
|
353
|
+
exists = connection.execute("SELECT 1 FROM runs WHERE id=?", (run_id,)).fetchone() is not None
|
|
354
|
+
for identifier in ids or ():
|
|
355
|
+
if not ID.fullmatch(str(identifier)):
|
|
356
|
+
continue
|
|
357
|
+
row = connection.execute("SELECT thread_id, run_id FROM attachments WHERE id=?",
|
|
358
|
+
(identifier,)).fetchone()
|
|
359
|
+
if row is None:
|
|
360
|
+
continue
|
|
361
|
+
if exists:
|
|
362
|
+
connection.execute("INSERT OR IGNORE INTO attachment_runs(attachment_id, run_id) VALUES (?, ?)",
|
|
363
|
+
(identifier, run_id))
|
|
364
|
+
connection.execute("UPDATE attachments SET run_id=COALESCE(run_id, ?), "
|
|
365
|
+
"thread_id=COALESCE(thread_id, ?) WHERE id=?",
|
|
366
|
+
(run_id if exists else None,
|
|
367
|
+
thread_id if thread_id and ID.fullmatch(str(thread_id)) else None, identifier))
|
|
368
|
+
recorded.append(identifier)
|
|
369
|
+
if recorded:
|
|
370
|
+
self.publish({"event": "attachments_used", "ids": recorded, "runId": run_id, "threadId": thread_id})
|
|
371
|
+
return recorded
|
|
372
|
+
|
|
373
|
+
def delete(self, attachment_id):
|
|
374
|
+
"""Delete one attachment and its files. True when it existed."""
|
|
375
|
+
return bool(self._delete([attachment_id]))
|
|
376
|
+
|
|
377
|
+
def _delete(self, ids):
|
|
378
|
+
ids = [i for i in ids if isinstance(i, str) and ID.fullmatch(i)]
|
|
379
|
+
if not ids:
|
|
380
|
+
return []
|
|
381
|
+
removed = []
|
|
382
|
+
with self.journal.transaction() as connection:
|
|
383
|
+
for identifier in ids:
|
|
384
|
+
if connection.execute("DELETE FROM attachments WHERE id=?", (identifier,)).rowcount:
|
|
385
|
+
removed.append(identifier)
|
|
386
|
+
for identifier in removed:
|
|
387
|
+
shutil.rmtree(self.folder(identifier), ignore_errors=True)
|
|
388
|
+
self.publish({"event": "attachment_deleted", "id": identifier})
|
|
389
|
+
return removed
|
|
390
|
+
|
|
391
|
+
def delete_thread(self, thread_id):
|
|
392
|
+
"""Delete every attachment of a conversation (it was deleted)."""
|
|
393
|
+
if not isinstance(thread_id, str) or not ID.fullmatch(thread_id):
|
|
394
|
+
return []
|
|
395
|
+
with self.journal.transaction() as connection:
|
|
396
|
+
ids = [r[0] for r in connection.execute("SELECT id FROM attachments WHERE thread_id=?", (thread_id,))]
|
|
397
|
+
return self._delete(ids)
|
|
398
|
+
|
|
399
|
+
def expired(self, run_ids):
|
|
400
|
+
"""Tasks left history: a file that no task uses any more and that belongs to no conversation goes with the last
|
|
401
|
+
of them. Only from service.py's clean-up thread: the expiry listener runs under the runtime's lock."""
|
|
402
|
+
if not run_ids:
|
|
403
|
+
return []
|
|
404
|
+
gone = set(run_ids)
|
|
405
|
+
with self.journal.transaction() as connection:
|
|
406
|
+
ids = [r[0] for r in connection.execute(
|
|
407
|
+
"SELECT id FROM attachments WHERE thread_id IS NULL AND run_id IS NOT NULL "
|
|
408
|
+
"AND NOT EXISTS (SELECT 1 FROM attachment_runs WHERE attachment_id=attachments.id)")]
|
|
409
|
+
named = self._referenced()
|
|
410
|
+
return self._delete([i for i in ids if not set(named.get(i, ())) - gone])
|
|
411
|
+
|
|
412
|
+
def gc(self, referenced=None, now=None):
|
|
413
|
+
"""Delete files nothing used within a day (``referenced``: ids a task names, kept; default: the tasks
|
|
414
|
+
in memory), files whose conversation is gone (when Mobster has conversations), and folders with no row (an
|
|
415
|
+
upload cut short)."""
|
|
416
|
+
now = self.clock() if now is None else now
|
|
417
|
+
referenced = set(self._referenced() if referenced is None else referenced)
|
|
418
|
+
with self.journal.transaction() as connection:
|
|
419
|
+
stale = [r[0] for r in connection.execute(
|
|
420
|
+
"SELECT id FROM attachments WHERE thread_id IS NULL AND run_id IS NULL AND created_at < ?",
|
|
421
|
+
(now - UNUSED_TTL,))]
|
|
422
|
+
stale += [r[0] for r in connection.execute(
|
|
423
|
+
"SELECT id FROM attachments WHERE thread_id IS NULL AND run_id IS NOT NULL "
|
|
424
|
+
"AND NOT EXISTS (SELECT 1 FROM attachment_runs WHERE attachment_id=attachments.id)")]
|
|
425
|
+
gone = []
|
|
426
|
+
if connection.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='threads'").fetchone():
|
|
427
|
+
gone = [r[0] for r in connection.execute(
|
|
428
|
+
"SELECT id FROM attachments WHERE thread_id IS NOT NULL "
|
|
429
|
+
"AND thread_id NOT IN (SELECT id FROM threads)")]
|
|
430
|
+
known = {r[0] for r in connection.execute("SELECT id FROM attachments")}
|
|
431
|
+
removed = self._delete([i for i in stale + gone if i not in referenced])
|
|
432
|
+
if self.root.is_dir():
|
|
433
|
+
for folder in self.root.iterdir():
|
|
434
|
+
if folder.name in known or folder.is_symlink() or not folder.is_dir() or not ID.fullmatch(folder.name):
|
|
435
|
+
continue
|
|
436
|
+
try:
|
|
437
|
+
if now - folder.stat().st_mtime > ORPHAN_TTL:
|
|
438
|
+
shutil.rmtree(folder, ignore_errors=True)
|
|
439
|
+
except OSError:
|
|
440
|
+
pass
|
|
441
|
+
return removed
|