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
mobile_agent/journal.py
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Private, crash-safe local run journal. Intent is durable before a device mutation."""
|
|
2
|
+
|
|
3
|
+
import fcntl
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sqlite3
|
|
10
|
+
import stat
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from . import usage
|
|
20
|
+
from .errors import MobsterError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JournalError(MobsterError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LeaseHeld(JournalError):
|
|
28
|
+
"""Another process holds this lease: the phone or the journal is in use, nothing is damaged."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message, owner=None):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.owner = owner
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def append_jsonl(path, event):
|
|
36
|
+
"""Durably append a private run event; the caller holds the run's lock."""
|
|
37
|
+
path = Path(path)
|
|
38
|
+
try:
|
|
39
|
+
fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
|
40
|
+
with os.fdopen(fd, "a", encoding="utf-8") as handle:
|
|
41
|
+
info = os.fstat(handle.fileno())
|
|
42
|
+
if info.st_uid != os.getuid() or not stat.S_ISREG(info.st_mode):
|
|
43
|
+
raise JournalError("Run journal has an unexpected owner or type")
|
|
44
|
+
os.fchmod(handle.fileno(), 0o600)
|
|
45
|
+
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
46
|
+
handle.flush()
|
|
47
|
+
os.fsync(handle.fileno())
|
|
48
|
+
# Persist the directory entry too when this append created the file.
|
|
49
|
+
directory = os.open(path.parent, os.O_RDONLY)
|
|
50
|
+
try:
|
|
51
|
+
os.fsync(directory)
|
|
52
|
+
finally:
|
|
53
|
+
os.close(directory)
|
|
54
|
+
except OSError:
|
|
55
|
+
raise JournalError("Run journal could not be saved; no further device action authorized") from None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# A Smart turn journals four events plus about two per action: 1,024 ended a 200-step run near step 150 (SPEC S5).
|
|
59
|
+
EVENT_LIMIT = 4096
|
|
60
|
+
RUN_BYTE_LIMIT = 16_000_000
|
|
61
|
+
TERMINAL_BYTE_LIMIT = 1_000_000
|
|
62
|
+
# Request keys kept for replay protection (see ``create``).
|
|
63
|
+
REQUEST_LIMIT = 10000
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# How long Lease.device tries again while a lease is held (a device listing's probe holds it for an instant).
|
|
67
|
+
DEVICE_LEASE_WAIT = 0.1
|
|
68
|
+
DEVICE_LEASE_RETRY = 0.02
|
|
69
|
+
# The test suites' lease folder. Never a setting or env var: two processes that disagreed on it could drive one phone.
|
|
70
|
+
_LEASE_ROOT = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Lease:
|
|
74
|
+
"""Process lease; never unlink a locked inode, which would allow a second owner."""
|
|
75
|
+
def __init__(self, path):
|
|
76
|
+
self.fd = None
|
|
77
|
+
self.legacy = [] # the same phone's locks as earlier builds name them (``device``)
|
|
78
|
+
try:
|
|
79
|
+
fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
80
|
+
info = os.fstat(fd)
|
|
81
|
+
if info.st_uid != os.getuid() or not stat.S_ISREG(info.st_mode):
|
|
82
|
+
os.close(fd)
|
|
83
|
+
raise JournalError("Lock file has an unexpected owner or type")
|
|
84
|
+
try:
|
|
85
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
86
|
+
except OSError:
|
|
87
|
+
os.close(fd)
|
|
88
|
+
raise LeaseHeld("Another Mobster process already owns this resource") from None
|
|
89
|
+
self.fd = fd
|
|
90
|
+
except OSError:
|
|
91
|
+
raise JournalError("Cannot acquire the private Mobster lease") from None
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def device(cls, identity, wait=DEVICE_LEASE_WAIT, udid=None):
|
|
95
|
+
"""The one lease per phone, whoever asks, in the data folder: $TMPDIR differs in a shell, cron and launchd."""
|
|
96
|
+
deadline = time.monotonic() + max(0.0, wait)
|
|
97
|
+
while True:
|
|
98
|
+
try:
|
|
99
|
+
return cls._device(identity, udid)
|
|
100
|
+
except LeaseHeld:
|
|
101
|
+
if time.monotonic() >= deadline:
|
|
102
|
+
raise
|
|
103
|
+
time.sleep(DEVICE_LEASE_RETRY)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def _lease_directory(cls):
|
|
107
|
+
from .paths import machine_data_dir
|
|
108
|
+
directory = (machine_data_dir() if _LEASE_ROOT is None else Path(_LEASE_ROOT)) / "device-leases"
|
|
109
|
+
directory.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
directory.mkdir(mode=0o700, exist_ok=True)
|
|
111
|
+
info = directory.lstat()
|
|
112
|
+
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077:
|
|
113
|
+
raise JournalError("Device lease directory must be private and owned by this user")
|
|
114
|
+
return directory
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def phone(cls, udid):
|
|
118
|
+
"""The one lease per phone UDID, whatever address reaches it; it records its holder for a refusal to name."""
|
|
119
|
+
path = cls._lease_directory() / ("phone-" + hashlib.sha256(str(udid).encode()).hexdigest() + ".lock")
|
|
120
|
+
try:
|
|
121
|
+
lease = cls(path)
|
|
122
|
+
except LeaseHeld:
|
|
123
|
+
owner = holder_of(path)
|
|
124
|
+
raise LeaseHeld(f"{owner} is driving this iPhone." if owner else
|
|
125
|
+
"Another Mobster process is driving this iPhone.", owner) from None
|
|
126
|
+
try:
|
|
127
|
+
os.ftruncate(lease.fd, 0)
|
|
128
|
+
os.pwrite(lease.fd, encode(own_record()).encode(), 0)
|
|
129
|
+
except OSError:
|
|
130
|
+
lease.close()
|
|
131
|
+
raise JournalError("Cannot record the phone's owner") from None
|
|
132
|
+
return lease
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def _device(cls, identity, udid=None):
|
|
136
|
+
directory = cls._lease_directory()
|
|
137
|
+
# The phone's own lease first: a refusal then names who has the phone, whichever address they reached it by.
|
|
138
|
+
phone = cls.phone(udid) if udid else None
|
|
139
|
+
try:
|
|
140
|
+
lease = cls(directory / (hashlib.sha256(device_identity(identity).encode()).hexdigest() + ".lock"))
|
|
141
|
+
except BaseException:
|
|
142
|
+
if phone is not None:
|
|
143
|
+
phone.close()
|
|
144
|
+
raise
|
|
145
|
+
if phone is not None:
|
|
146
|
+
lease.legacy.append(phone)
|
|
147
|
+
# Builds up to 0.1.0 lock the address as spelled in the temporary folder; one may still drive this phone.
|
|
148
|
+
try:
|
|
149
|
+
for spelling in legacy_device_spellings(identity):
|
|
150
|
+
held = cls._legacy_device(spelling)
|
|
151
|
+
if held is not None:
|
|
152
|
+
lease.legacy.append(held)
|
|
153
|
+
except BaseException:
|
|
154
|
+
lease.close()
|
|
155
|
+
raise
|
|
156
|
+
return lease
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def _legacy_device(cls, spelling):
|
|
160
|
+
"""The earlier builds' lock for this spelling, or None when its folder is missing or not private."""
|
|
161
|
+
root = tempfile.gettempdir() if _LEASE_ROOT is None else _LEASE_ROOT
|
|
162
|
+
directory = Path(root) / f"mobster-device-leases-{os.getuid()}"
|
|
163
|
+
try:
|
|
164
|
+
directory.mkdir(mode=0o700, exist_ok=True)
|
|
165
|
+
info = directory.lstat()
|
|
166
|
+
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077:
|
|
167
|
+
return None
|
|
168
|
+
return cls(directory / (hashlib.sha256(spelling.encode()).hexdigest() + ".lock"))
|
|
169
|
+
except LeaseHeld:
|
|
170
|
+
raise
|
|
171
|
+
except (OSError, JournalError):
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def close(self):
|
|
175
|
+
for held in self.legacy:
|
|
176
|
+
held.close()
|
|
177
|
+
self.legacy = []
|
|
178
|
+
if self.fd is not None:
|
|
179
|
+
os.close(self.fd)
|
|
180
|
+
self.fd = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _command_of(pid):
|
|
184
|
+
"""The exact command line of a live process, or None."""
|
|
185
|
+
try:
|
|
186
|
+
done = subprocess.run(["/bin/ps", "-p", str(int(pid)), "-o", "command="], capture_output=True, text=True,
|
|
187
|
+
timeout=2)
|
|
188
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
189
|
+
return None
|
|
190
|
+
text = done.stdout.strip()
|
|
191
|
+
return text if done.returncode == 0 and text else None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def own_record():
|
|
195
|
+
"""What a phone's lease says about this process."""
|
|
196
|
+
return {"pid": os.getpid(), "command": _command_of(os.getpid()) or " ".join(sys.argv)}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def describe(command):
|
|
200
|
+
"""A process's command line in words a person knows: the Mac app, a dev window, `mobster serve`, `mobster run`."""
|
|
201
|
+
text = str(command)
|
|
202
|
+
if "Mobster.app" in text or "mobster-agent" in text:
|
|
203
|
+
return "The Mobster app"
|
|
204
|
+
for word in ("serve", "run", "mcp", "verify"):
|
|
205
|
+
if re.search(rf"\bmobile_agent\b.*\b{word}\b|\bmobster\b.*\b{word}\b", text):
|
|
206
|
+
return f"`mobster {word}`"
|
|
207
|
+
return "Another Mobster (" + text[:60] + ")"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def holder_of(path):
|
|
211
|
+
"""Who holds the lease at ``path``, in words, or None unless its recorded pid still runs its recorded command."""
|
|
212
|
+
try:
|
|
213
|
+
record = json.loads(Path(path).read_text())
|
|
214
|
+
pid, command = int(record["pid"]), str(record["command"])
|
|
215
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
216
|
+
return None
|
|
217
|
+
if _command_of(pid) != command:
|
|
218
|
+
return None
|
|
219
|
+
return f"{describe(command)} (pid {pid})"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def legacy_device_spellings(identity):
|
|
223
|
+
"""The names builds up to 0.1.0 gave this phone's lock."""
|
|
224
|
+
text = str(identity)
|
|
225
|
+
if text.startswith("/"):
|
|
226
|
+
return [str(Path(text).resolve())]
|
|
227
|
+
return list(dict.fromkeys((text, text.rstrip("/"))))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def device_identity(identity):
|
|
231
|
+
"""One name per phone however it is spelled: a socket path resolved, a URL as host and port, loopback as one."""
|
|
232
|
+
import ipaddress
|
|
233
|
+
from urllib.parse import urlsplit
|
|
234
|
+
text = str(identity)
|
|
235
|
+
if text.startswith("/"):
|
|
236
|
+
return str(Path(text).resolve())
|
|
237
|
+
parts = urlsplit(text.strip())
|
|
238
|
+
if parts.scheme.lower() not in {"http", "https"} or not parts.hostname:
|
|
239
|
+
return text
|
|
240
|
+
host = parts.hostname.lower().rstrip(".")
|
|
241
|
+
try:
|
|
242
|
+
address = ipaddress.ip_address(host.split("%", 1)[0])
|
|
243
|
+
address = getattr(address, "ipv4_mapped", None) or address
|
|
244
|
+
host = "loopback" if address.is_loopback else address.compressed
|
|
245
|
+
except ValueError:
|
|
246
|
+
if host == "localhost" or host.endswith(".localhost"):
|
|
247
|
+
host = "loopback"
|
|
248
|
+
try:
|
|
249
|
+
port = parts.port
|
|
250
|
+
except ValueError:
|
|
251
|
+
return text
|
|
252
|
+
port = port or (443 if parts.scheme.lower() == "https" else 80)
|
|
253
|
+
return f"wda://{host}:{port}"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def encode(value):
|
|
257
|
+
return json.dumps(value, allow_nan=False, separators=(",", ":"))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
log = logging.getLogger("mobster.journal")
|
|
261
|
+
|
|
262
|
+
# Track tables in the run journal (seam S5): name -> (version, migrate). Registered by a track's register(api).
|
|
263
|
+
SCHEMAS: dict = {}
|
|
264
|
+
SCHEMA_OWNERS: dict = {} # name -> the track that registered it (tracks.STATUS key)
|
|
265
|
+
SCHEMA_NAME = re.compile(r"[a-z_]{3,32}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def disable_track(track, reason):
|
|
269
|
+
"""Turn ``track`` off because its schema can't be used."""
|
|
270
|
+
from . import harness_api
|
|
271
|
+
harness_api.hooks().track_disabled(track, reason)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def register_schema(name, version, migrate):
|
|
275
|
+
"""A track's tables: ``migrate(conn, from_version)`` runs inside BEGIN IMMEDIATE and alters only its own tables."""
|
|
276
|
+
if not isinstance(name, str) or not SCHEMA_NAME.fullmatch(name):
|
|
277
|
+
raise ValueError("A schema's name is 3 to 32 lowercase letters or underscores")
|
|
278
|
+
if not isinstance(version, int) or isinstance(version, bool) or version < 1:
|
|
279
|
+
raise ValueError("A schema's version is a whole number from 1")
|
|
280
|
+
if not callable(migrate):
|
|
281
|
+
raise ValueError("A schema needs migrate(conn, from_version)")
|
|
282
|
+
if name in SCHEMAS:
|
|
283
|
+
raise ValueError(f"The schema {name} is already registered")
|
|
284
|
+
from . import harness_api
|
|
285
|
+
SCHEMAS[name] = (version, migrate)
|
|
286
|
+
SCHEMA_OWNERS[name] = harness_api._owner[0] or name
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class Journal:
|
|
290
|
+
def __init__(self, path=None):
|
|
291
|
+
self.lock = threading.RLock()
|
|
292
|
+
self.lease = None
|
|
293
|
+
self.connection = None
|
|
294
|
+
self.expiry_listeners = []
|
|
295
|
+
self.schema_status = {} # schema name -> "ok" | "newer" | "migration_failed"
|
|
296
|
+
try:
|
|
297
|
+
if path is not None:
|
|
298
|
+
path = Path(path).absolute()
|
|
299
|
+
path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
300
|
+
if path.is_symlink():
|
|
301
|
+
raise JournalError("The journal cannot be a symbolic link")
|
|
302
|
+
self.lease = Lease(str(path) + ".lock")
|
|
303
|
+
fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
304
|
+
info = os.fstat(fd)
|
|
305
|
+
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid():
|
|
306
|
+
os.close(fd)
|
|
307
|
+
raise JournalError("Journal has an unexpected owner or type")
|
|
308
|
+
os.fchmod(fd, 0o600)
|
|
309
|
+
os.close(fd)
|
|
310
|
+
self.connection = sqlite3.connect(str(path) if path else ":memory:",
|
|
311
|
+
timeout=3, check_same_thread=False, isolation_level=None)
|
|
312
|
+
self.connection.execute("PRAGMA foreign_keys=ON")
|
|
313
|
+
self.connection.execute("PRAGMA journal_mode=WAL")
|
|
314
|
+
self.connection.execute("PRAGMA synchronous=FULL")
|
|
315
|
+
version = self.connection.execute("PRAGMA user_version").fetchone()[0]
|
|
316
|
+
if version not in (0, 1):
|
|
317
|
+
raise JournalError("Unsupported journal version; no device action authorized")
|
|
318
|
+
self.connection.executescript("""
|
|
319
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
320
|
+
id TEXT PRIMARY KEY, data TEXT NOT NULL, bytes INTEGER NOT NULL DEFAULT 0);
|
|
321
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
322
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
323
|
+
seq INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY(run_id, seq));
|
|
324
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
325
|
+
key TEXT PRIMARY KEY, run_id TEXT NOT NULL, fingerprint TEXT NOT NULL);
|
|
326
|
+
CREATE TABLE IF NOT EXISTS schema_versions (
|
|
327
|
+
name TEXT PRIMARY KEY, version INTEGER NOT NULL);
|
|
328
|
+
PRAGMA user_version=1;
|
|
329
|
+
""")
|
|
330
|
+
self._migrate_schemas()
|
|
331
|
+
usage.initialize(self.connection)
|
|
332
|
+
except LeaseHeld:
|
|
333
|
+
# In use, not damaged: say so rather than suggest the data is at risk.
|
|
334
|
+
self.close()
|
|
335
|
+
raise LeaseHeld("Another Mobster process has this task history open") from None
|
|
336
|
+
except Exception:
|
|
337
|
+
self.close()
|
|
338
|
+
raise JournalError("Cannot open the run journal; existing data was not reset") from None
|
|
339
|
+
|
|
340
|
+
def _migrate_schemas(self):
|
|
341
|
+
"""Bring each schema up to date in its own transaction. Never raises: a failing one turns off its own track."""
|
|
342
|
+
for name in sorted(SCHEMAS):
|
|
343
|
+
version, migrate = SCHEMAS[name]
|
|
344
|
+
track = SCHEMA_OWNERS.get(name, name)
|
|
345
|
+
try:
|
|
346
|
+
row = self.connection.execute("SELECT version FROM schema_versions WHERE name=?", (name,)).fetchone()
|
|
347
|
+
except sqlite3.Error:
|
|
348
|
+
self.schema_status[name] = "migration_failed"
|
|
349
|
+
disable_track(track, "error: migration")
|
|
350
|
+
continue
|
|
351
|
+
stored = row[0] if row else 0
|
|
352
|
+
if stored > version:
|
|
353
|
+
self.schema_status[name] = "newer"
|
|
354
|
+
disable_track(track, "error: newer data")
|
|
355
|
+
continue
|
|
356
|
+
if stored == version:
|
|
357
|
+
self.schema_status[name] = "ok"
|
|
358
|
+
continue
|
|
359
|
+
try:
|
|
360
|
+
self.connection.execute("BEGIN IMMEDIATE")
|
|
361
|
+
migrate(self.connection, stored)
|
|
362
|
+
self.connection.execute(
|
|
363
|
+
"INSERT INTO schema_versions(name, version) VALUES (?, ?) "
|
|
364
|
+
"ON CONFLICT(name) DO UPDATE SET version=excluded.version", (name, version))
|
|
365
|
+
self.connection.execute("COMMIT")
|
|
366
|
+
self.schema_status[name] = "ok"
|
|
367
|
+
except Exception: # noqa: BLE001 -- a track's migration never blocks the run journal
|
|
368
|
+
log.exception("the %s schema could not migrate from version %s", name, stored)
|
|
369
|
+
if self.connection.in_transaction:
|
|
370
|
+
self.connection.execute("ROLLBACK")
|
|
371
|
+
self.schema_status[name] = "migration_failed"
|
|
372
|
+
disable_track(track, "error: migration")
|
|
373
|
+
|
|
374
|
+
def add_expiry_listener(self, fn):
|
|
375
|
+
"""``fn(run_ids)`` after create()'s retention and delete() commit, with the removed run ids."""
|
|
376
|
+
if not callable(fn):
|
|
377
|
+
raise ValueError("An expiry listener is a function")
|
|
378
|
+
self.expiry_listeners.append(fn)
|
|
379
|
+
|
|
380
|
+
def _expired(self, run_ids):
|
|
381
|
+
if not run_ids:
|
|
382
|
+
return
|
|
383
|
+
for fn in list(self.expiry_listeners):
|
|
384
|
+
try:
|
|
385
|
+
fn(list(run_ids))
|
|
386
|
+
except Exception: # noqa: BLE001
|
|
387
|
+
log.exception("an expiry listener failed")
|
|
388
|
+
|
|
389
|
+
@contextmanager
|
|
390
|
+
def transaction(self):
|
|
391
|
+
with self.lock:
|
|
392
|
+
try:
|
|
393
|
+
self.connection.execute("BEGIN IMMEDIATE")
|
|
394
|
+
yield self.connection
|
|
395
|
+
self.connection.execute("COMMIT")
|
|
396
|
+
except BaseException as exc:
|
|
397
|
+
if self.connection and self.connection.in_transaction:
|
|
398
|
+
self.connection.execute("ROLLBACK")
|
|
399
|
+
if isinstance(exc, JournalError) or not isinstance(exc, Exception):
|
|
400
|
+
raise
|
|
401
|
+
raise JournalError("Run journal could not be saved; no further device action authorized") from None
|
|
402
|
+
|
|
403
|
+
def lookup(self, key):
|
|
404
|
+
with self.lock:
|
|
405
|
+
return self.connection.execute("SELECT run_id, fingerprint FROM requests WHERE key=?", (key,)).fetchone()
|
|
406
|
+
|
|
407
|
+
def create(self, metadata, key=None, fingerprint=""):
|
|
408
|
+
with self.transaction() as connection:
|
|
409
|
+
if key:
|
|
410
|
+
self._make_room(connection)
|
|
411
|
+
connection.execute("INSERT INTO runs(id,data) VALUES (?,?)", (metadata["id"], encode(metadata)))
|
|
412
|
+
if key:
|
|
413
|
+
connection.execute("INSERT INTO requests VALUES (?,?,?)", (key, metadata["id"], fingerprint))
|
|
414
|
+
# Keep every unfinished task's intent, plus the newest 100 finished tasks. A slow task can outlive
|
|
415
|
+
# many short tasks on other phones; deleting it would make its next action impossible to journal.
|
|
416
|
+
expired, finished = [], 0
|
|
417
|
+
for identifier, data in connection.execute("SELECT id,data FROM runs ORDER BY rowid DESC"):
|
|
418
|
+
try:
|
|
419
|
+
finished_at = json.loads(data).get("finishedAt")
|
|
420
|
+
except (TypeError, ValueError, AttributeError):
|
|
421
|
+
continue # an old or malformed row has no trustworthy terminal marker
|
|
422
|
+
if type(finished_at) in (int, float) and finished_at > 0:
|
|
423
|
+
finished += 1
|
|
424
|
+
if finished > 100:
|
|
425
|
+
expired.append(identifier)
|
|
426
|
+
connection.executemany("DELETE FROM runs WHERE id=?", [(identifier,) for identifier in expired])
|
|
427
|
+
# Request tombstones deliberately survive run retention, preventing replay.
|
|
428
|
+
self._expired(expired)
|
|
429
|
+
return expired
|
|
430
|
+
|
|
431
|
+
@staticmethod
|
|
432
|
+
def _make_room(connection):
|
|
433
|
+
"""Keep at most REQUEST_LIMIT request keys; a key whose run is still in history is never forgotten."""
|
|
434
|
+
excess = connection.execute("SELECT count(*) FROM requests").fetchone()[0] - REQUEST_LIMIT + 1
|
|
435
|
+
if excess > 0:
|
|
436
|
+
connection.execute("""DELETE FROM requests WHERE rowid IN (SELECT rowid FROM requests
|
|
437
|
+
WHERE run_id NOT IN (SELECT id FROM runs) ORDER BY rowid LIMIT ?)""", (excess,))
|
|
438
|
+
if connection.execute("SELECT count(*) FROM requests").fetchone()[0] >= REQUEST_LIMIT:
|
|
439
|
+
raise JournalError("Idempotency journal reached its retention limit; operator maintenance required")
|
|
440
|
+
|
|
441
|
+
def append(self, metadata, event):
|
|
442
|
+
data = encode(event)
|
|
443
|
+
size = len(data.encode())
|
|
444
|
+
if size > 1_000_000 or event["seq"] >= EVENT_LIMIT:
|
|
445
|
+
raise JournalError("Run event budget exceeded; no further device action authorized")
|
|
446
|
+
with self.transaction() as connection:
|
|
447
|
+
row = connection.execute("SELECT bytes,data FROM runs WHERE id=?", (metadata["id"],)).fetchone()
|
|
448
|
+
if not row or row[0] + size > RUN_BYTE_LIMIT:
|
|
449
|
+
raise JournalError("Run journal budget exceeded; no further device action authorized")
|
|
450
|
+
if json.loads(row[1])["finishedAt"] is not None:
|
|
451
|
+
raise JournalError("A finished run cannot accept further events")
|
|
452
|
+
connection.execute("INSERT INTO events VALUES (?,?,?)", (metadata["id"], event["seq"], data))
|
|
453
|
+
usage.record_call(connection, metadata["id"], event)
|
|
454
|
+
connection.execute("UPDATE runs SET data=?,bytes=bytes+? WHERE id=?",
|
|
455
|
+
(encode(metadata), size, metadata["id"]))
|
|
456
|
+
|
|
457
|
+
def finish(self, metadata, event):
|
|
458
|
+
"""One reserved terminal record; ordinary event exhaustion cannot strand recovery."""
|
|
459
|
+
data, final = encode(event), encode(metadata)
|
|
460
|
+
if (event["event"] != "run_finished" or metadata["finishedAt"] is None
|
|
461
|
+
or metadata["status"] in {"queued", "running"}
|
|
462
|
+
or event["status"] != metadata["status"]
|
|
463
|
+
or len(data.encode()) > TERMINAL_BYTE_LIMIT or len(final.encode()) > TERMINAL_BYTE_LIMIT):
|
|
464
|
+
raise JournalError("Invalid or oversized terminal run record")
|
|
465
|
+
with self.transaction() as connection:
|
|
466
|
+
row = connection.execute("SELECT data FROM runs WHERE id=?", (metadata["id"],)).fetchone()
|
|
467
|
+
if not row or json.loads(row[0])["finishedAt"] is not None:
|
|
468
|
+
raise JournalError("Run is missing or already finished")
|
|
469
|
+
count = connection.execute("SELECT count(*) FROM events WHERE run_id=?", (metadata["id"],)).fetchone()[0]
|
|
470
|
+
if event["seq"] != count or count > EVENT_LIMIT:
|
|
471
|
+
raise JournalError("Terminal event does not follow the saved run history")
|
|
472
|
+
connection.execute("INSERT INTO events VALUES (?,?,?)", (metadata["id"], count, data))
|
|
473
|
+
connection.execute("UPDATE runs SET data=?,bytes=bytes+? WHERE id=?",
|
|
474
|
+
(final, len(data.encode()), metadata["id"]))
|
|
475
|
+
usage.close_pending(connection, metadata["id"], event["timestamp"])
|
|
476
|
+
|
|
477
|
+
def load(self):
|
|
478
|
+
with self.lock:
|
|
479
|
+
records = []
|
|
480
|
+
try:
|
|
481
|
+
for identifier, data in self.connection.execute("SELECT id,data FROM runs ORDER BY rowid"):
|
|
482
|
+
record = json.loads(data)
|
|
483
|
+
record["events"] = [json.loads(row[0]) for row in self.connection.execute(
|
|
484
|
+
"SELECT data FROM events WHERE run_id=? ORDER BY seq", (identifier,))]
|
|
485
|
+
if [e["seq"] for e in record["events"]] != list(range(len(record["events"]))):
|
|
486
|
+
raise ValueError("Non-contiguous journal")
|
|
487
|
+
records.append(record)
|
|
488
|
+
return records
|
|
489
|
+
except (ValueError, KeyError, TypeError, sqlite3.Error):
|
|
490
|
+
raise JournalError("Run journal is unreadable; no automatic recovery or replay attempted") from None
|
|
491
|
+
|
|
492
|
+
def usage(self, run_id=None):
|
|
493
|
+
with self.lock:
|
|
494
|
+
return usage.summary(self.connection, run_id)
|
|
495
|
+
|
|
496
|
+
def spend_since(self, since_ms):
|
|
497
|
+
"""Published-rate spend across the workspace since ``since_ms`` (see usage.spend_since)."""
|
|
498
|
+
with self.lock:
|
|
499
|
+
return usage.spend_since(self.connection, since_ms)
|
|
500
|
+
|
|
501
|
+
def plan_since(self, since_ms):
|
|
502
|
+
"""Tokens run on the person's plans since ``since_ms`` and their API-equivalent cost (see usage.plan_since)."""
|
|
503
|
+
with self.lock:
|
|
504
|
+
return usage.plan_since(self.connection, since_ms)
|
|
505
|
+
|
|
506
|
+
def delete(self, run_id):
|
|
507
|
+
"""Remove a run and its events; usage rows and request tombstones stay, so a replay never starts it again."""
|
|
508
|
+
with self.transaction() as connection:
|
|
509
|
+
deleted = connection.execute("DELETE FROM runs WHERE id=?", (run_id,)).rowcount == 1
|
|
510
|
+
if deleted:
|
|
511
|
+
self._expired([run_id])
|
|
512
|
+
return deleted
|
|
513
|
+
|
|
514
|
+
def annotate(self, run_id, changes):
|
|
515
|
+
"""Merge ``changes`` into a saved run's metadata (a finished run's review mark)."""
|
|
516
|
+
with self.transaction() as connection:
|
|
517
|
+
row = connection.execute("SELECT data FROM runs WHERE id=?", (run_id,)).fetchone()
|
|
518
|
+
if not row:
|
|
519
|
+
raise JournalError("Run is missing")
|
|
520
|
+
data = json.loads(row[0])
|
|
521
|
+
data.update(changes)
|
|
522
|
+
connection.execute("UPDATE runs SET data=? WHERE id=?", (encode(data), run_id))
|
|
523
|
+
|
|
524
|
+
def close(self):
|
|
525
|
+
with self.lock:
|
|
526
|
+
if self.connection is not None:
|
|
527
|
+
self.connection.close()
|
|
528
|
+
self.connection = None
|
|
529
|
+
if self.lease:
|
|
530
|
+
self.lease.close()
|
|
531
|
+
self.lease = None
|