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,380 @@
|
|
|
1
|
+
"""Workflows as files: each project's `.mobster/workflows/*.yaml` and the workflows Mobster runs, in step both ways.
|
|
2
|
+
|
|
3
|
+
Every 1.5 seconds Mobster looks at each project's workflow files (a stat each; a file is read only when it changed)
|
|
4
|
+
and at the workflows that have a file (the record's ``file``: where it is, the hash and the published revision last
|
|
5
|
+
in step, and how it stands). Then, for each file:
|
|
6
|
+
|
|
7
|
+
- **A new file** becomes a workflow, published, its schedule off until you turn it on. A file that doesn't parse or
|
|
8
|
+
check stays out and is listed with its problem.
|
|
9
|
+
- **The file changed** (a pull, an editor): its graph is checked as any save is and published as a new revision, so
|
|
10
|
+
Versions keeps the one before. If the workflow has an unpublished draft, or was published since, nothing is
|
|
11
|
+
overwritten: the file is ``conflict`` until you resolve it.
|
|
12
|
+
- **The workflow was published** (Publish, the card builder, Bring it back) or renamed: the file is written, unless
|
|
13
|
+
it changed on disk since (``conflict``) or the graph names a real iPhone or holds a secret (``blocked``).
|
|
14
|
+
- **The file was deleted**: the workflow stays and says so (``missing``); publishing writes the file again.
|
|
15
|
+
- **The workflow was deleted in Mobster**: its file goes too, only when it is as Mobster last left it.
|
|
16
|
+
|
|
17
|
+
Saving a draft never writes the file. Nothing is retried: a write that failed says why on the next look. The rest
|
|
18
|
+
of a workflow (its phone, profile, destination, schedule switch, pins) stays on the Mac. Events: ``workflow_file``
|
|
19
|
+
on the ``workflows`` topic.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from ..api_errors import APIError
|
|
28
|
+
from ..paths import write_text_atomic
|
|
29
|
+
from . import folder
|
|
30
|
+
from .workflow_files import MAX_BYTES, SUFFIXES, FileError, blocked, digest, parse, render, slug
|
|
31
|
+
|
|
32
|
+
INTERVAL = 1.5
|
|
33
|
+
MAX_FILES = 200
|
|
34
|
+
FOLDER = Path(".mobster") / "workflows"
|
|
35
|
+
TOO_BIG = "is larger than 512 KB, so Mobster didn't read it."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PATH_PROBLEM = ("The workflow path contains a link or leaves this project. Keep .mobster/workflows and its files "
|
|
39
|
+
"inside the project without links, then publish again.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _safe_path(root, path):
|
|
43
|
+
"""Workflow files stay under their registered project and never follow a link, including a parent folder."""
|
|
44
|
+
root, path = Path(root), Path(path)
|
|
45
|
+
try:
|
|
46
|
+
relative = path.relative_to(root)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
49
|
+
return ".." not in relative.parts and not any(
|
|
50
|
+
root.joinpath(*relative.parts[:index]).is_symlink() for index in range(len(relative.parts) + 1))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _live(record):
|
|
54
|
+
published = record.get("published")
|
|
55
|
+
return published["graph"] if isinstance(published, dict) and isinstance(published.get("graph"), dict) \
|
|
56
|
+
else record["graph"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _published_revision(record):
|
|
60
|
+
published = record.get("published")
|
|
61
|
+
return published.get("revision") if isinstance(published, dict) else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WorkflowSync:
|
|
65
|
+
"""One project's workflow files and their workflows, for every project of a ProjectService."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, service, workflows, *, interval=INTERVAL, clock=time.time):
|
|
68
|
+
self.service, self.workflows = service, workflows
|
|
69
|
+
self.interval, self.clock = interval, clock
|
|
70
|
+
self._files = {} # absolute path -> (mtime_ns, size, text or None, hash or None)
|
|
71
|
+
self._invalid = {} # (project id, path) -> (hash, sentence): new files that don't parse or check
|
|
72
|
+
self._lock = threading.RLock()
|
|
73
|
+
self._stop = threading.Event()
|
|
74
|
+
self._thread = None
|
|
75
|
+
workflows.deleted.append(self._deleted)
|
|
76
|
+
|
|
77
|
+
# -- the loop --------------------------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def start(self):
|
|
80
|
+
self._thread = threading.Thread(target=self._loop, name="mobster-workflow-files", daemon=True)
|
|
81
|
+
self._thread.start()
|
|
82
|
+
|
|
83
|
+
def close(self):
|
|
84
|
+
self._stop.set()
|
|
85
|
+
if self._deleted in self.workflows.deleted:
|
|
86
|
+
self.workflows.deleted.remove(self._deleted)
|
|
87
|
+
if self._thread is not None:
|
|
88
|
+
self._thread.join(2)
|
|
89
|
+
|
|
90
|
+
def _loop(self):
|
|
91
|
+
while not self._stop.wait(self.interval):
|
|
92
|
+
try:
|
|
93
|
+
self.tick()
|
|
94
|
+
except Exception: # noqa: BLE001 -- one bad look never stops the next
|
|
95
|
+
from .. import harness_api
|
|
96
|
+
harness_api.log.exception("workflow files: a look failed")
|
|
97
|
+
|
|
98
|
+
def tick(self):
|
|
99
|
+
"""Look once at every project's workflow files and bring each pair in step."""
|
|
100
|
+
with self._lock:
|
|
101
|
+
records = self.workflows.file_records()
|
|
102
|
+
for project in self.service.store.projects():
|
|
103
|
+
if folder.state(project["path"], self.service.home)[0] != "ready":
|
|
104
|
+
continue
|
|
105
|
+
mine = [record for record in records if record["file"].get("projectId") == project["id"]]
|
|
106
|
+
self._project(project, mine)
|
|
107
|
+
|
|
108
|
+
def _project(self, project, records):
|
|
109
|
+
root = Path(project["path"])
|
|
110
|
+
if not _safe_path(root, root / FOLDER):
|
|
111
|
+
for record in records:
|
|
112
|
+
self._mark(project, record, "blocked", PATH_PROBLEM)
|
|
113
|
+
return
|
|
114
|
+
files = {}
|
|
115
|
+
try:
|
|
116
|
+
entries = sorted(os.scandir(root / FOLDER), key=lambda entry: entry.name)
|
|
117
|
+
except OSError:
|
|
118
|
+
entries = []
|
|
119
|
+
for entry in entries[:MAX_FILES]:
|
|
120
|
+
if (not entry.name.startswith(".") and entry.name.lower().endswith(SUFFIXES)
|
|
121
|
+
and entry.is_file(follow_symlinks=False)):
|
|
122
|
+
files[(FOLDER / entry.name).as_posix()] = Path(entry.path)
|
|
123
|
+
links = {record["file"].get("path"): record for record in records}
|
|
124
|
+
for path, absolute in files.items():
|
|
125
|
+
text, hashed = self._read(absolute, project["path"])
|
|
126
|
+
record = links.pop(path, None)
|
|
127
|
+
if record is None:
|
|
128
|
+
self._new(project, path, text, hashed)
|
|
129
|
+
else:
|
|
130
|
+
self._pair(project, record, path, absolute, text, hashed)
|
|
131
|
+
for path, record in links.items():
|
|
132
|
+
self._gone(project, record, root / path)
|
|
133
|
+
known = set(files)
|
|
134
|
+
for key in [key for key in self._invalid if key[0] == project["id"] and key[1] not in known]:
|
|
135
|
+
self._invalid.pop(key, None)
|
|
136
|
+
|
|
137
|
+
def _read(self, absolute, root):
|
|
138
|
+
"""(text, hash) of a file, read again only when its size or time changed; (None, None) when it can't be read,
|
|
139
|
+
("", TOO_BIG) when it is too large."""
|
|
140
|
+
if not _safe_path(root, absolute):
|
|
141
|
+
return None, None
|
|
142
|
+
try:
|
|
143
|
+
status = absolute.stat()
|
|
144
|
+
except OSError:
|
|
145
|
+
return None, None
|
|
146
|
+
kept = self._files.get(str(absolute))
|
|
147
|
+
if kept and kept[:2] == (status.st_mtime_ns, status.st_size):
|
|
148
|
+
return kept[2], kept[3]
|
|
149
|
+
if status.st_size > MAX_BYTES:
|
|
150
|
+
text, hashed = "", TOO_BIG
|
|
151
|
+
else:
|
|
152
|
+
try:
|
|
153
|
+
text = absolute.read_text(encoding="utf-8")
|
|
154
|
+
hashed = digest(text)
|
|
155
|
+
except (OSError, UnicodeDecodeError):
|
|
156
|
+
text, hashed = None, None
|
|
157
|
+
self._files[str(absolute)] = (status.st_mtime_ns, status.st_size, text, hashed)
|
|
158
|
+
return text, hashed
|
|
159
|
+
|
|
160
|
+
# -- each case -------------------------------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def _link(self, project, path, record, *, hashed, revision, state="synced", problem=None):
|
|
163
|
+
file = {"projectId": project["id"], "projectName": project["name"], "path": path, "hash": hashed,
|
|
164
|
+
"revision": revision, "name": record["name"], "state": state, "problem": problem,
|
|
165
|
+
"syncedAt": round(self.clock() * 1000)}
|
|
166
|
+
before = record.get("file") or {}
|
|
167
|
+
if all(before.get(key) == file[key] for key in ("hash", "revision", "name", "state", "problem", "path")):
|
|
168
|
+
return
|
|
169
|
+
if self.workflows.set_file(record["id"], file) is not None:
|
|
170
|
+
self._publish(record["id"], state, revision)
|
|
171
|
+
|
|
172
|
+
def _mark(self, project, record, state, problem):
|
|
173
|
+
before = record["file"]
|
|
174
|
+
self._link(project, before["path"], record, hashed=before.get("hash"), revision=before.get("revision"),
|
|
175
|
+
state=state, problem=problem)
|
|
176
|
+
|
|
177
|
+
def _new(self, project, path, text, hashed):
|
|
178
|
+
key = (project["id"], path)
|
|
179
|
+
if text is None or self._invalid.get(key, (None,))[0] == hashed:
|
|
180
|
+
return
|
|
181
|
+
if hashed == TOO_BIG:
|
|
182
|
+
self._invalid[key] = (hashed, f"{path} {TOO_BIG}")
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
name, graph = parse(text, path)
|
|
186
|
+
created = self.workflows.create({"name": name, "graph": graph, "publish": True})
|
|
187
|
+
except (FileError, ValueError, APIError) as error:
|
|
188
|
+
self._invalid[key] = (hashed, _sentence(path, error))
|
|
189
|
+
self._publish(None, "invalid", None, path=path, projectId=project["id"])
|
|
190
|
+
return
|
|
191
|
+
self._invalid.pop(key, None)
|
|
192
|
+
self._link(project, path, {**created, "file": None}, hashed=hashed, revision=created["revision"])
|
|
193
|
+
|
|
194
|
+
def _pair(self, project, record, path, absolute, text, hashed):
|
|
195
|
+
file = record["file"]
|
|
196
|
+
if text is None:
|
|
197
|
+
return # unreadable for a moment: the next look tells
|
|
198
|
+
file_changed = hashed != file.get("hash")
|
|
199
|
+
app_changed = _published_revision(record) != file.get("revision") or record["name"] != file.get("name")
|
|
200
|
+
if hashed == TOO_BIG:
|
|
201
|
+
if file_changed:
|
|
202
|
+
self._mark(project, record, "invalid", f"{path} {TOO_BIG}")
|
|
203
|
+
return
|
|
204
|
+
if file.get("state") == "conflict":
|
|
205
|
+
return # it waits for resolve()
|
|
206
|
+
if file_changed and (app_changed or record["graph"] != _live(record)):
|
|
207
|
+
self._mark(project, record, "conflict",
|
|
208
|
+
f"{path} changed while this workflow had changes of its own in Mobster. Keep the file's version "
|
|
209
|
+
"or Mobster's.")
|
|
210
|
+
elif file_changed:
|
|
211
|
+
self._take(project, record, path, text, hashed)
|
|
212
|
+
elif app_changed:
|
|
213
|
+
self._write(project, record, path, absolute)
|
|
214
|
+
elif file.get("state") != "synced":
|
|
215
|
+
self._mark(project, record, "synced", None)
|
|
216
|
+
|
|
217
|
+
def _take(self, project, record, path, text, hashed):
|
|
218
|
+
"""The file's graph becomes the published one (a new revision; Versions keeps the one before)."""
|
|
219
|
+
if file_problem := self._invalid.get((project["id"], path)):
|
|
220
|
+
if file_problem[0] == hashed:
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
name, graph = parse(text, path, previous=record["graph"])
|
|
224
|
+
saved = self.workflows.update(record["id"], {"revision": record["revision"], "graph": graph,
|
|
225
|
+
"publish": True, "name": name})
|
|
226
|
+
except APIError as error:
|
|
227
|
+
if error.code == "revision_conflict":
|
|
228
|
+
return # saved meanwhile: the next look sees both changes
|
|
229
|
+
self._mark(project, record, "invalid", _sentence(path, error))
|
|
230
|
+
return
|
|
231
|
+
except (FileError, ValueError) as error:
|
|
232
|
+
self._invalid[(project["id"], path)] = (hashed, _sentence(path, error))
|
|
233
|
+
file = record["file"]
|
|
234
|
+
self._link(project, path, record, hashed=file.get("hash"), revision=file.get("revision"), state="invalid",
|
|
235
|
+
problem=_sentence(path, error))
|
|
236
|
+
return
|
|
237
|
+
self._invalid.pop((project["id"], path), None)
|
|
238
|
+
self._link(project, path, {**record, "name": saved["name"]}, hashed=hashed, revision=saved["revision"])
|
|
239
|
+
|
|
240
|
+
def _write(self, project, record, path, absolute):
|
|
241
|
+
"""The workflow's published graph is written to its file."""
|
|
242
|
+
if not _safe_path(project["path"], absolute):
|
|
243
|
+
self._mark(project, record, "blocked", PATH_PROBLEM)
|
|
244
|
+
return
|
|
245
|
+
graph = _live(record)
|
|
246
|
+
reason = blocked(graph)
|
|
247
|
+
if reason:
|
|
248
|
+
self._mark(project, record, "blocked", reason)
|
|
249
|
+
return
|
|
250
|
+
text = render(record["name"], graph)
|
|
251
|
+
try:
|
|
252
|
+
write_text_atomic(absolute, text)
|
|
253
|
+
except OSError as error:
|
|
254
|
+
self._mark(project, record, "blocked", f"Mobster couldn't write {path}: {error.strerror or error}. Check "
|
|
255
|
+
"the folder, then publish again.")
|
|
256
|
+
return
|
|
257
|
+
try:
|
|
258
|
+
status = absolute.stat()
|
|
259
|
+
self._files[str(absolute)] = (status.st_mtime_ns, status.st_size, text, digest(text))
|
|
260
|
+
except OSError:
|
|
261
|
+
pass
|
|
262
|
+
self._link(project, path, record, hashed=digest(text), revision=_published_revision(record))
|
|
263
|
+
|
|
264
|
+
def _gone(self, project, record, absolute):
|
|
265
|
+
file = record["file"]
|
|
266
|
+
if _published_revision(record) != file.get("revision") or record["name"] != file.get("name"):
|
|
267
|
+
self._write(project, record, file["path"], absolute)
|
|
268
|
+
elif file.get("state") != "missing":
|
|
269
|
+
self._mark(project, record, "missing",
|
|
270
|
+
f"{file['path']} isn't in the project any more. Publish the workflow to write it again, or "
|
|
271
|
+
"delete the workflow.")
|
|
272
|
+
|
|
273
|
+
def _deleted(self, record):
|
|
274
|
+
"""A workflow deleted in Mobster takes its file with it, only when the file is as Mobster last left it."""
|
|
275
|
+
file = record.get("file")
|
|
276
|
+
if not isinstance(file, dict):
|
|
277
|
+
return
|
|
278
|
+
project = self.service.store.project(file.get("projectId"))
|
|
279
|
+
if project is None:
|
|
280
|
+
return
|
|
281
|
+
absolute = Path(project["path"]) / file["path"]
|
|
282
|
+
with self._lock:
|
|
283
|
+
text, hashed = self._read(absolute, project["path"])
|
|
284
|
+
if text is not None and hashed == file.get("hash"):
|
|
285
|
+
try:
|
|
286
|
+
absolute.unlink()
|
|
287
|
+
except OSError:
|
|
288
|
+
return
|
|
289
|
+
self._files.pop(str(absolute), None)
|
|
290
|
+
|
|
291
|
+
# -- asked from the API ----------------------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def _record(self, workflow_id):
|
|
294
|
+
record = next((item for item in self.workflows.file_records() if item["id"] == workflow_id), None)
|
|
295
|
+
if record is None:
|
|
296
|
+
raise APIError("That workflow has no file in a project.", 404, "not_found")
|
|
297
|
+
project = self.service.store.project(record["file"].get("projectId"))
|
|
298
|
+
if project is None:
|
|
299
|
+
raise APIError("That workflow's project isn't in Mobster any more.", 404, "not_found")
|
|
300
|
+
return record, project
|
|
301
|
+
|
|
302
|
+
def resolve(self, workflow_id, keep):
|
|
303
|
+
"""POST /api/projects/workflows/{id}/resolve {keep}: "file" publishes the file over Mobster's changes (they
|
|
304
|
+
stay in Versions); "mobster" publishes the workflow's draft and writes it over the file."""
|
|
305
|
+
if keep not in ("file", "mobster"):
|
|
306
|
+
raise ValueError('keep is "file" or "mobster"')
|
|
307
|
+
with self._lock:
|
|
308
|
+
record, project = self._record(workflow_id)
|
|
309
|
+
path = record["file"]["path"]
|
|
310
|
+
absolute = Path(project["path"]) / path
|
|
311
|
+
if keep == "file":
|
|
312
|
+
text, hashed = self._read(absolute, project["path"])
|
|
313
|
+
if text is None:
|
|
314
|
+
raise APIError(f"{path} isn't there to keep. Keep Mobster's version instead.", 409, "missing")
|
|
315
|
+
record = {**record, "file": {**record["file"], "state": "synced"}}
|
|
316
|
+
self._take(project, record, path, text, hashed)
|
|
317
|
+
else:
|
|
318
|
+
if record["graph"] != _live(record):
|
|
319
|
+
self.workflows.update(record["id"], {"revision": record["revision"], "graph": record["graph"],
|
|
320
|
+
"publish": True})
|
|
321
|
+
record = next(item for item in self.workflows.file_records() if item["id"] == workflow_id)
|
|
322
|
+
self._write(project, {**record, "file": {**record["file"], "revision": None}}, path, absolute)
|
|
323
|
+
return self.workflows.get(workflow_id)
|
|
324
|
+
|
|
325
|
+
def add(self, project, workflow_id):
|
|
326
|
+
"""POST /api/projects/{id}/workflows {workflowId}: the workflow gets a file in the project, written from its
|
|
327
|
+
published graph on the next look."""
|
|
328
|
+
if not isinstance(workflow_id, str):
|
|
329
|
+
raise ValueError("Send the workflow's id")
|
|
330
|
+
with self._lock:
|
|
331
|
+
workflow = self.workflows.get(workflow_id)
|
|
332
|
+
if workflow.get("file"):
|
|
333
|
+
raise APIError(f"{workflow['name']} already has a file: {workflow['file']['path']}.", 409, "exists")
|
|
334
|
+
directory = Path(project["path"]) / FOLDER
|
|
335
|
+
taken = {entry.name for entry in _entries(directory)} if _safe_path(project["path"], directory) else set()
|
|
336
|
+
taken.update(Path(record["file"]["path"]).name for record in self.workflows.file_records()
|
|
337
|
+
if record["file"].get("projectId") == project["id"])
|
|
338
|
+
path = (FOLDER / slug(workflow["name"], taken)).as_posix()
|
|
339
|
+
self.workflows.set_file(workflow_id, {"projectId": project["id"], "projectName": project["name"],
|
|
340
|
+
"path": path, "hash": None, "revision": None,
|
|
341
|
+
"name": workflow["name"], "state": "pending", "problem": None,
|
|
342
|
+
"syncedAt": None})
|
|
343
|
+
self._publish(workflow_id, "pending", None)
|
|
344
|
+
return self.workflows.get(workflow_id)
|
|
345
|
+
|
|
346
|
+
def forget(self, project_id):
|
|
347
|
+
"""The project was removed from Mobster: its workflows stay, without a file."""
|
|
348
|
+
with self._lock:
|
|
349
|
+
for record in self.workflows.file_records():
|
|
350
|
+
if record["file"].get("projectId") == project_id:
|
|
351
|
+
self.workflows.set_file(record["id"], None)
|
|
352
|
+
for key in [key for key in self._invalid if key[0] == project_id]:
|
|
353
|
+
self._invalid.pop(key, None)
|
|
354
|
+
|
|
355
|
+
def files(self, project_id):
|
|
356
|
+
"""The project's workflow files: each workflow's and each file that isn't one, with its problem."""
|
|
357
|
+
out = [{"path": record["file"]["path"], "workflowId": record["id"], "name": record["name"],
|
|
358
|
+
"state": record["file"].get("state"), "problem": record["file"].get("problem")}
|
|
359
|
+
for record in self.workflows.file_records() if record["file"].get("projectId") == project_id]
|
|
360
|
+
out += [{"path": path, "workflowId": None, "name": None, "state": "invalid", "problem": problem}
|
|
361
|
+
for (project, path), (_, problem) in self._invalid.items() if project == project_id]
|
|
362
|
+
return sorted(out, key=lambda item: item["path"])
|
|
363
|
+
|
|
364
|
+
def _publish(self, workflow_id, state, revision, **fields):
|
|
365
|
+
publish = getattr(self.service.runtime, "publish", None)
|
|
366
|
+
if callable(publish):
|
|
367
|
+
publish("workflows", {"event": "workflow_file", "workflowId": workflow_id, "state": state,
|
|
368
|
+
"revision": revision, **fields})
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _entries(path):
|
|
372
|
+
try:
|
|
373
|
+
return list(os.scandir(path))
|
|
374
|
+
except OSError:
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _sentence(path, error):
|
|
379
|
+
text = str(error)
|
|
380
|
+
return text if text.startswith(path) else f"{path}: {text}"
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""A workflow as a file in its project: `.mobster/workflows/<name>.yaml`, written so a pull request's diff reads.
|
|
2
|
+
|
|
3
|
+
``render(name, graph)`` writes the published graph; ``parse(text, where)`` reads a file back into (name, graph) or
|
|
4
|
+
raises FileError naming the line. The graph a file gives still goes through every check a save does (graph/schema.py)
|
|
5
|
+
before Mobster takes it. The format, version 1::
|
|
6
|
+
|
|
7
|
+
version: 1
|
|
8
|
+
name: Answer DMs
|
|
9
|
+
inputs:
|
|
10
|
+
- {name: who, type: string, default: Sam}
|
|
11
|
+
limits: {maxSpendUsd: 2, maxMinutes: 60}
|
|
12
|
+
nodes:
|
|
13
|
+
- id: start
|
|
14
|
+
type: trigger.manual
|
|
15
|
+
at: [0, 0]
|
|
16
|
+
- id: read
|
|
17
|
+
type: phone.task
|
|
18
|
+
at: [0, 152]
|
|
19
|
+
config:
|
|
20
|
+
text: Open Instagram and read the newest DM from {who}
|
|
21
|
+
edges:
|
|
22
|
+
- {from: start, to: read}
|
|
23
|
+
- {from: check.asked, to: tell}
|
|
24
|
+
|
|
25
|
+
Keys always come in the same order, a node's config leaves out what is at its default, an edge names its ends as
|
|
26
|
+
``node`` or ``node.port`` (``out`` and ``in`` are left out), and a moved node changes one line. What stays on the Mac
|
|
27
|
+
and never goes in the file: the workflow's phone, profile and destination, whether its schedule is on, and pins (test
|
|
28
|
+
data). ``blocked(graph)`` refuses a graph that names a real iPhone or holds a secret, so neither reaches a repository.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import functools
|
|
32
|
+
import hashlib
|
|
33
|
+
import re
|
|
34
|
+
|
|
35
|
+
from ..graph import catalog
|
|
36
|
+
from ..secret_filter import redact
|
|
37
|
+
|
|
38
|
+
VERSION = 1
|
|
39
|
+
MAX_BYTES = 512_000
|
|
40
|
+
SUFFIXES = (".yaml", ".yml")
|
|
41
|
+
SHARED_DEVICES = (None, "any", "loop")
|
|
42
|
+
TOP = ("version", "name", "inputs", "vars", "limits", "nodes", "edges")
|
|
43
|
+
NODE = ("id", "type", "title", "notes", "parent", "at", "retry", "config")
|
|
44
|
+
EDGE = ("from", "to", "map", "id")
|
|
45
|
+
HEADER = "# A Mobster workflow. Mobster for Mac keeps this file and its canvas in step: edit either.\n"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FileError(ValueError):
|
|
49
|
+
"""A file that isn't a workflow: one sentence, with the file and the line where there is one."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def digest(text):
|
|
53
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def slug(name, taken=()):
|
|
57
|
+
"""The file name for a workflow called ``name``: lowercase words joined by dashes, unique among ``taken``."""
|
|
58
|
+
base = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", name.lower())).strip("-")[:60].strip("-") or "workflow"
|
|
59
|
+
candidate, n = f"{base}.yaml", 2
|
|
60
|
+
taken = {name.casefold() for name in taken}
|
|
61
|
+
while candidate in taken:
|
|
62
|
+
candidate, n = f"{base}-{n}.yaml", n + 1
|
|
63
|
+
return candidate
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def edge_id(source, source_port, target, target_port):
|
|
67
|
+
"""An edge's id when its file gives none: the same ends always give the same id."""
|
|
68
|
+
key = f"{source}.{source_port}>{target}.{target_port}"
|
|
69
|
+
return "e" + hashlib.sha256(key.encode()).hexdigest()[:10]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# -- writing -------------------------------------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _config(node):
|
|
75
|
+
"""The node's config without the fields at their default, in the catalog's order."""
|
|
76
|
+
node_type = catalog.TYPES.get(node["type"])
|
|
77
|
+
config = dict(node.get("config") or {})
|
|
78
|
+
if node_type is None:
|
|
79
|
+
return config
|
|
80
|
+
out = {}
|
|
81
|
+
for name, field in node_type.fields.items():
|
|
82
|
+
if name in config:
|
|
83
|
+
value = config.pop(name)
|
|
84
|
+
if field.required or value != field.default:
|
|
85
|
+
out[name] = value
|
|
86
|
+
out.update(config)
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _end(node, port, default):
|
|
91
|
+
return node if port == default else f"{node}.{port}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def blocked(graph):
|
|
95
|
+
"""Why this graph can't go in a repository file, or None: a step names a real iPhone, or a value is a secret."""
|
|
96
|
+
for node in graph.get("nodes") or ():
|
|
97
|
+
config = node.get("config") or {}
|
|
98
|
+
if config.get("device") not in SHARED_DEVICES:
|
|
99
|
+
return (f"The step {node.get('title') or node['id']} runs on one named iPhone, and a project's files never "
|
|
100
|
+
"name a real phone. Set it to the workflow's phone, then publish.")
|
|
101
|
+
for text in _strings(graph):
|
|
102
|
+
if redact(text) != text:
|
|
103
|
+
return ("The workflow holds what looks like a key, a password or a card number, and Mobster never writes "
|
|
104
|
+
"one into a project. Keep it in the project's secrets (.mobster/env.example), then publish.")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _strings(value):
|
|
109
|
+
pending = [value]
|
|
110
|
+
while pending:
|
|
111
|
+
item = pending.pop()
|
|
112
|
+
if isinstance(item, str):
|
|
113
|
+
yield item
|
|
114
|
+
elif isinstance(item, dict):
|
|
115
|
+
pending.extend(item.values())
|
|
116
|
+
elif isinstance(item, list):
|
|
117
|
+
pending.extend(item)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def document(name, graph):
|
|
121
|
+
"""The file's content as data, in the format's order."""
|
|
122
|
+
doc = {"version": VERSION, "name": name}
|
|
123
|
+
if graph.get("inputs"):
|
|
124
|
+
doc["inputs"] = [{key: item[key] for key in ("name", "type", "default", "description", "required")
|
|
125
|
+
if item.get(key) is not None} for item in graph["inputs"]]
|
|
126
|
+
if graph.get("vars"):
|
|
127
|
+
doc["vars"] = graph["vars"]
|
|
128
|
+
doc["limits"] = {key: _number(value) for key, value in (graph.get("limits") or catalog.DEFAULT_LIMITS).items()}
|
|
129
|
+
nodes = []
|
|
130
|
+
for node in graph.get("nodes") or ():
|
|
131
|
+
item = {"id": node["id"], "type": node["type"]}
|
|
132
|
+
for key in ("title", "notes", "parent"):
|
|
133
|
+
if node.get(key):
|
|
134
|
+
item[key] = node[key]
|
|
135
|
+
position = node.get("position") or {}
|
|
136
|
+
item["at"] = _Flow([_number(position.get("x", 0)), _number(position.get("y", 0))])
|
|
137
|
+
if node.get("retry"):
|
|
138
|
+
item["retry"] = node["retry"]
|
|
139
|
+
config = _config(node)
|
|
140
|
+
if config:
|
|
141
|
+
item["config"] = config
|
|
142
|
+
nodes.append(item)
|
|
143
|
+
doc["nodes"] = nodes
|
|
144
|
+
edges = []
|
|
145
|
+
for edge in graph.get("edges") or ():
|
|
146
|
+
item = {"from": _end(edge["source"], edge["sourcePort"], "out"),
|
|
147
|
+
"to": _end(edge["target"], edge["targetPort"], "in")}
|
|
148
|
+
if edge.get("map") is not None:
|
|
149
|
+
item["map"] = edge["map"]
|
|
150
|
+
edges.append(item)
|
|
151
|
+
doc["edges"] = edges
|
|
152
|
+
return doc
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def render(name, graph):
|
|
156
|
+
"""The file for a workflow called ``name`` whose published graph is ``graph``."""
|
|
157
|
+
import yaml
|
|
158
|
+
return HEADER + yaml.dump(document(name, graph), Dumper=_dumper(), sort_keys=False, allow_unicode=True, width=120,
|
|
159
|
+
default_flow_style=False)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _number(value):
|
|
163
|
+
return int(value) if isinstance(value, float) and value.is_integer() else value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class _Flow(list):
|
|
167
|
+
"""A list written on one line: a node's ``at``."""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@functools.cache
|
|
171
|
+
def _dumper():
|
|
172
|
+
"""yaml's safe dumper, with a node's ``at`` on one line. Built on first use, so yaml loads only when a file is
|
|
173
|
+
written."""
|
|
174
|
+
import yaml
|
|
175
|
+
|
|
176
|
+
class Dumper(yaml.SafeDumper):
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
Dumper.add_representer(_Flow, lambda dumper, value: dumper.represent_sequence(
|
|
180
|
+
"tag:yaml.org,2002:seq", value, flow_style=True))
|
|
181
|
+
return Dumper
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# -- reading -------------------------------------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def parse(text, where, *, previous=None):
|
|
187
|
+
"""(name, graph) from a file's text. ``where`` names the file in sentences. ``previous`` is the workflow's graph
|
|
188
|
+
as Mobster has it: its edges keep their ids and its nodes their pins when the file leaves them as they were.
|
|
189
|
+
FileError with what is wrong; the graph is then checked as any save is."""
|
|
190
|
+
import yaml
|
|
191
|
+
if len(text.encode("utf-8")) > MAX_BYTES:
|
|
192
|
+
raise FileError(f"{where} is larger than {MAX_BYTES // 1000} KB.")
|
|
193
|
+
try:
|
|
194
|
+
data = yaml.safe_load(text)
|
|
195
|
+
except yaml.YAMLError as error:
|
|
196
|
+
mark = getattr(error, "problem_mark", None)
|
|
197
|
+
line = f" line {mark.line + 1}" if mark is not None else ""
|
|
198
|
+
raise FileError(f"{where}{line}: {getattr(error, 'problem', None) or 'not valid YAML'}") from None
|
|
199
|
+
except (RecursionError, ValueError):
|
|
200
|
+
raise FileError(f"{where}: nested too deeply") from None
|
|
201
|
+
if not isinstance(data, dict):
|
|
202
|
+
raise FileError(f"{where} is empty or isn't a workflow: it needs version, name, nodes and edges.")
|
|
203
|
+
unknown = [key for key in data if key not in TOP]
|
|
204
|
+
if unknown:
|
|
205
|
+
raise FileError(f"{where}: {unknown[0]!r} isn't a workflow field. A workflow has " + ", ".join(TOP) + ".")
|
|
206
|
+
if data.get("version") != VERSION:
|
|
207
|
+
raise FileError(f"{where}: this is a version {data.get('version')!r} file, and Mobster reads version "
|
|
208
|
+
f"{VERSION}. Update Mobster, or set version: 1.")
|
|
209
|
+
name = data.get("name")
|
|
210
|
+
if not isinstance(name, str) or not 1 <= len(name.strip()) <= 120:
|
|
211
|
+
raise FileError(f"{where}: name is the workflow's name, 1 to 120 characters.")
|
|
212
|
+
previous_nodes = {node["id"]: node for node in (previous or {}).get("nodes") or ()}
|
|
213
|
+
nodes = []
|
|
214
|
+
for index, item in enumerate(_list(data, "nodes", where)):
|
|
215
|
+
if not isinstance(item, dict):
|
|
216
|
+
raise FileError(f"{where}: node {index + 1} is a mapping with id, type and at.")
|
|
217
|
+
unknown = [key for key in item if key not in NODE]
|
|
218
|
+
if unknown:
|
|
219
|
+
raise FileError(f"{where}: node {item.get('id', index + 1)} has {unknown[0]!r}; a node has "
|
|
220
|
+
+ ", ".join(NODE) + ".")
|
|
221
|
+
node = {key: item[key] for key in ("id", "type", "title", "notes", "parent", "retry") if key in item}
|
|
222
|
+
at = item.get("at", [0, 0])
|
|
223
|
+
if not (isinstance(at, list) and len(at) == 2
|
|
224
|
+
and all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in at)):
|
|
225
|
+
raise FileError(f"{where}: node {item.get('id', index + 1)}'s at is [x, y], two numbers.")
|
|
226
|
+
node["position"] = {"x": at[0], "y": at[1]}
|
|
227
|
+
node["config"] = item.get("config") if item.get("config") is not None else {}
|
|
228
|
+
if not isinstance(node.get("id"), str):
|
|
229
|
+
raise FileError(f"{where}: node {index + 1}'s id is text.")
|
|
230
|
+
before = previous_nodes.get(node["id"])
|
|
231
|
+
if before is not None and before.get("type") == node.get("type") and before.get("pin") is not None:
|
|
232
|
+
node["pin"] = before["pin"]
|
|
233
|
+
nodes.append(node)
|
|
234
|
+
ids = {node.get("id") for node in nodes}
|
|
235
|
+
previous_edges = {(e["source"], e["sourcePort"], e["target"], e["targetPort"]): e["id"]
|
|
236
|
+
for e in (previous or {}).get("edges") or ()}
|
|
237
|
+
edges = []
|
|
238
|
+
for index, item in enumerate(_list(data, "edges", where)):
|
|
239
|
+
if not isinstance(item, dict) or not isinstance(item.get("from"), str) or not isinstance(item.get("to"), str):
|
|
240
|
+
raise FileError(f"{where}: edge {index + 1} is {{from: node, to: node}}, with .port after a node when it "
|
|
241
|
+
"isn't out or in.")
|
|
242
|
+
unknown = [key for key in item if key not in EDGE]
|
|
243
|
+
if unknown:
|
|
244
|
+
raise FileError(f"{where}: edge {index + 1} has {unknown[0]!r}; an edge has from, to and map.")
|
|
245
|
+
source, source_port = _split(item["from"], "out")
|
|
246
|
+
target, target_port = _split(item["to"], "in")
|
|
247
|
+
for end in (source, target):
|
|
248
|
+
if end not in ids:
|
|
249
|
+
raise FileError(f"{where}: edge {index + 1} joins {end!r}, and no node has that id.")
|
|
250
|
+
key = (source, source_port, target, target_port)
|
|
251
|
+
edge = {"id": item.get("id") or previous_edges.get(key) or edge_id(*key), "source": source,
|
|
252
|
+
"sourcePort": source_port, "target": target, "targetPort": target_port}
|
|
253
|
+
if item.get("map") is not None:
|
|
254
|
+
edge["map"] = item["map"]
|
|
255
|
+
edges.append(edge)
|
|
256
|
+
graph = {"version": catalog.VERSION, "inputs": data.get("inputs") or [], "vars": data.get("vars") or {},
|
|
257
|
+
"nodes": nodes, "edges": edges, "limits": data.get("limits") or dict(catalog.DEFAULT_LIMITS)}
|
|
258
|
+
return " ".join(name.split()), graph
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _list(data, key, where):
|
|
262
|
+
value = data.get(key) if data.get(key) is not None else []
|
|
263
|
+
if not isinstance(value, list):
|
|
264
|
+
raise FileError(f"{where}: {key} is a list.")
|
|
265
|
+
return value
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _split(end, default):
|
|
269
|
+
node, dot, port = end.strip().partition(".")
|
|
270
|
+
return node, (port if dot else default)
|