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,178 @@
|
|
|
1
|
+
"""Two workflow steps that act on the phone by code, with no model: "Open a link" (``phone.open_url``) and "Wait for a
|
|
2
|
+
notification" (``phone.notification``). Each rides a run field (``openUrl``, ``waitNotification``) that the step's task
|
|
3
|
+
carries, and a pre-run hook ends the run with the step's result, as the app pack read does (packs/register.py).
|
|
4
|
+
|
|
5
|
+
Open a link goes through the same rules as every other link (deeplinks.decide): a web address or an app Mobster knows
|
|
6
|
+
opens; a link that acts or writes to someone (tel:, sms:, a shortcut) asks, and with nobody to answer is not opened;
|
|
7
|
+
a scheme Mobster never opens is refused when the workflow is saved. The app it opens must be one the person allows.
|
|
8
|
+
|
|
9
|
+
Wait for a notification reads Notification Center every POLL_S seconds until a notification from the app (when named)
|
|
10
|
+
holds the words asked for, or the time is up. A match opens it when ``tap`` is on; no match in time is not a failure:
|
|
11
|
+
the step's ``timeout`` port runs. Notification text is data: codes are masked, and nothing in it instructs anything.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from .. import deeplinks, harness_api
|
|
17
|
+
|
|
18
|
+
ORDER = 30 # after the front door and packs (2, 3); before the learned paths (20+)
|
|
19
|
+
POLL_S = 15.0
|
|
20
|
+
MAX_MINUTES = 240
|
|
21
|
+
URL_CHARS = 2000
|
|
22
|
+
MATCH_CHARS = 200
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_open_url(value, runtime):
|
|
26
|
+
"""``openUrl``: {"url"}, a link Mobster may open (a link that asks is allowed here: the hook asks)."""
|
|
27
|
+
if not isinstance(value, dict) or set(value) != {"url"} or not isinstance(value["url"], str):
|
|
28
|
+
raise ValueError("openUrl is {url}")
|
|
29
|
+
url = value["url"].strip()
|
|
30
|
+
if not url or len(url) > URL_CHARS:
|
|
31
|
+
raise ValueError(f"openUrl's url is 1 to {URL_CHARS} characters")
|
|
32
|
+
deeplinks.risk_of(url) # LinkError (a ValueError): a scheme Mobster never opens
|
|
33
|
+
return {"url": url}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_wait_notification(value, runtime):
|
|
37
|
+
"""``waitNotification``: {"app": bundle id | None, "match": words | None, "timeoutMinutes", "tap"}."""
|
|
38
|
+
allowed = {"app", "match", "timeoutMinutes", "tap"}
|
|
39
|
+
if not isinstance(value, dict) or set(value) - allowed:
|
|
40
|
+
raise ValueError("waitNotification is {app, match, timeoutMinutes, tap}")
|
|
41
|
+
app, match = value.get("app"), value.get("match")
|
|
42
|
+
minutes, tap = value.get("timeoutMinutes", 10), value.get("tap", True)
|
|
43
|
+
if app is not None and (not isinstance(app, str) or not 1 <= len(app) <= 255):
|
|
44
|
+
raise ValueError("waitNotification's app is an app's bundle id")
|
|
45
|
+
if match is not None and (not isinstance(match, str) or len(match) > MATCH_CHARS):
|
|
46
|
+
raise ValueError(f"waitNotification's match is at most {MATCH_CHARS} characters")
|
|
47
|
+
if type(minutes) is not int or not 1 <= minutes <= MAX_MINUTES:
|
|
48
|
+
raise ValueError(f"waitNotification waits 1 to {MAX_MINUTES} minutes")
|
|
49
|
+
if type(tap) is not bool:
|
|
50
|
+
raise ValueError("waitNotification's tap is true or false")
|
|
51
|
+
return {"app": app, "match": " ".join(match.split()) if match and match.strip() else None,
|
|
52
|
+
"timeoutMinutes": minutes, "tap": tap}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def register():
|
|
56
|
+
"""Idempotent."""
|
|
57
|
+
fields = harness_api.run_fields()
|
|
58
|
+
if "openUrl" not in fields:
|
|
59
|
+
harness_api.register_run_field("openUrl", validate_open_url, model=False)
|
|
60
|
+
harness_api.register_pre_run(open_url_hook, order=ORDER)
|
|
61
|
+
if "waitNotification" not in fields:
|
|
62
|
+
harness_api.register_run_field("waitNotification", validate_wait_notification, model=False)
|
|
63
|
+
harness_api.register_pre_run(notification_hook, order=ORDER + 1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _result(status, outcome, answer, reason=None, data=None, started=None, actions=1):
|
|
67
|
+
return harness_api.PreRun(result={
|
|
68
|
+
"event": "result", "status": status, "engine": "smart", "outcome": outcome, "answer": answer, "reason": reason,
|
|
69
|
+
"data": data, "actions": actions, "decisions": 0, "costUsd": 0.0, "model": None,
|
|
70
|
+
"elapsed_ms": round((time.monotonic() - started) * 1000) if started else 0,
|
|
71
|
+
"independently_verified": False, "proof": []})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _refused(reason, started):
|
|
75
|
+
return _result("completion_not_confirmed", "check", None, reason, started=started, actions=0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _app_allowed(ctx, bundle):
|
|
79
|
+
allows = getattr(ctx.runtime, "allows_app", None)
|
|
80
|
+
return True if not bundle or ctx.runtime is None or not callable(allows) else bool(allows(ctx.run_id, bundle))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def open_url_hook(ctx, driver, approve, clock=time.monotonic):
|
|
84
|
+
spec = (ctx.extras or {}).get("openUrl")
|
|
85
|
+
if not spec or driver is None:
|
|
86
|
+
return None
|
|
87
|
+
started = clock()
|
|
88
|
+
url = spec["url"]
|
|
89
|
+
try:
|
|
90
|
+
decision = deeplinks.decide(url)
|
|
91
|
+
except deeplinks.LinkError as error:
|
|
92
|
+
return _refused(f"{error} Mobster didn't open it.", started)
|
|
93
|
+
# A web address with no app of its own opens in Safari; that is the app that must be allowed.
|
|
94
|
+
bundle = deeplinks.app_for(url) or ("com.apple.mobilesafari" if url.casefold().startswith(("http://", "https://"))
|
|
95
|
+
else None)
|
|
96
|
+
if not _app_allowed(ctx, bundle):
|
|
97
|
+
return _refused("That link opens an app you haven't allowed, so Mobster didn't open it. Allow the app, then "
|
|
98
|
+
"run the step again.", started)
|
|
99
|
+
if decision.action == "ask":
|
|
100
|
+
request = {"kind": "commit", "step": 1, "operation": "OPEN_URL", "label": f"Open {url[:180]}", "role": "Link",
|
|
101
|
+
"text": url[:500], "title": decision.title or "Open this link?", "act": "open link",
|
|
102
|
+
"app_name": decision.app_name or None}
|
|
103
|
+
answer = "denied"
|
|
104
|
+
if approve is not None:
|
|
105
|
+
try:
|
|
106
|
+
answer = str(approve(request) or "denied")
|
|
107
|
+
except Exception: # noqa: BLE001 -- no answer is no
|
|
108
|
+
answer = "denied"
|
|
109
|
+
if answer != "approved":
|
|
110
|
+
return _refused("That link acts or writes to someone, and nobody approved it, so Mobster didn't open it.",
|
|
111
|
+
started)
|
|
112
|
+
ctx.emit({"event": "action_started", "operation": "OPEN_URL", "target": url[:80]})
|
|
113
|
+
try:
|
|
114
|
+
opened = deeplinks.open_link(driver, url, bundle=bundle)
|
|
115
|
+
snapshot = driver.observe(timeout=10)
|
|
116
|
+
except Exception as error: # noqa: BLE001 -- the link may have opened: never opened again by this step
|
|
117
|
+
return _result("completion_not_confirmed", "check", None,
|
|
118
|
+
f"Mobster sent the link, but couldn't confirm it opened ({type(error).__name__}). It won't "
|
|
119
|
+
"send it again; check the phone.", started=started)
|
|
120
|
+
from ..contract_screen import screen_title
|
|
121
|
+
landed = getattr(snapshot, "bundle_id", "") or opened.get("bundleId") or ""
|
|
122
|
+
data = {"url": url, "landedApp": landed, "screen": (screen_title(snapshot) or "")[:120]}
|
|
123
|
+
return _result("completed", "done", f"Opened {url[:120]}.", data=data, started=started)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _matches(row, app_bundle, words):
|
|
127
|
+
from ..catalog import app_name
|
|
128
|
+
if app_bundle:
|
|
129
|
+
name = (app_name(app_bundle) or "").casefold()
|
|
130
|
+
if not name or name not in str(row.app or "").casefold():
|
|
131
|
+
return False
|
|
132
|
+
return not words or words.casefold() in " ".join(str(row.text or "").split()).casefold()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def notification_hook(ctx, driver, approve, clock=time.monotonic, sleep=time.sleep, center=None):
|
|
136
|
+
spec = (ctx.extras or {}).get("waitNotification")
|
|
137
|
+
if not spec or driver is None:
|
|
138
|
+
return None
|
|
139
|
+
from ..skills.notifications import NotificationCenter
|
|
140
|
+
started = clock()
|
|
141
|
+
if not _app_allowed(ctx, spec.get("app")):
|
|
142
|
+
return _refused("That notification is from an app you haven't allowed, so Mobster didn't wait for it.", started)
|
|
143
|
+
center = center or NotificationCenter(driver)
|
|
144
|
+
deadline = started + spec["timeoutMinutes"] * 60
|
|
145
|
+
ctx.emit({"event": "action_started", "operation": "WAIT_NOTIFICATION", "target": (spec.get("app") or "")[:80]})
|
|
146
|
+
while True:
|
|
147
|
+
if ctx.cancelled():
|
|
148
|
+
return _refused("Stopped while waiting for a notification.", started)
|
|
149
|
+
try:
|
|
150
|
+
snapshot = driver.observe(timeout=10)
|
|
151
|
+
rows = center.read(origin=getattr(snapshot, "bundle_id", "") or "", size=snapshot, keep_partial=True)
|
|
152
|
+
except Exception as error: # noqa: BLE001 -- a read that fails is tried again until the time is up
|
|
153
|
+
rows = []
|
|
154
|
+
ctx.emit({"event": "notification_read_failed", "error": type(error).__name__})
|
|
155
|
+
found = next((row for row in rows if _matches(row, spec.get("app"), spec.get("match"))), None)
|
|
156
|
+
if found is not None:
|
|
157
|
+
public = found.public(now=time.time())
|
|
158
|
+
if spec["tap"]:
|
|
159
|
+
try:
|
|
160
|
+
tapped = center.open_notification(found.text, origin=getattr(snapshot, "bundle_id", "") or "",
|
|
161
|
+
size=snapshot)
|
|
162
|
+
except Exception as error: # noqa: BLE001 -- it may have opened: never tapped again by this step
|
|
163
|
+
return _result("completion_not_confirmed", "check", None,
|
|
164
|
+
f"Mobster found the notification but couldn't confirm it opened it "
|
|
165
|
+
f"({type(error).__name__}). It won't tap it again; check the phone.",
|
|
166
|
+
started=started)
|
|
167
|
+
if not tapped:
|
|
168
|
+
return _result("completion_not_confirmed", "check", None,
|
|
169
|
+
"Mobster found the notification, but it was gone before it could open it.",
|
|
170
|
+
started=started)
|
|
171
|
+
data = {"title": public["title"], "body": public["body"], "app": public["app"], "at": public["time"],
|
|
172
|
+
"tapped": bool(spec["tap"])}
|
|
173
|
+
return _result("completed", "done", f"Got a notification: {public['title'][:80]}.", data=data,
|
|
174
|
+
started=started)
|
|
175
|
+
if clock() + POLL_S >= deadline:
|
|
176
|
+
return _result("completed", "done", "No matching notification arrived in time.",
|
|
177
|
+
data={"timeout": True}, started=started, actions=0)
|
|
178
|
+
sleep(POLL_S)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Test runs and re-runs from the editor (docs/workflows.mdx › Debug a workflow).
|
|
2
|
+
|
|
3
|
+
- **A test run** (``test``) runs the draft the canvas edits, and a node with a ``pin`` doesn't run: it is done at once
|
|
4
|
+
with its pinned output. Any other run runs the published graph and never reads pins.
|
|
5
|
+
- **From a node** (``{"from": {"batchId", "node", "input"?}}``): the node and everything after it run again; every other
|
|
6
|
+
node takes the result it had in that run. A node it needs that didn't finish there can't be reused, so the re-run is
|
|
7
|
+
refused and says which.
|
|
8
|
+
- **One round** (``{"from": {"batchId", "node": <loop>, "iteration": i}}``): that loop runs round ``i`` alone, with
|
|
9
|
+
the item it had then; nothing after the loop runs.
|
|
10
|
+
- **Only** (``{"only": {"node", "batchId"?}}``): that node runs on the input it had, and nothing after it.
|
|
11
|
+
- **Resume** (``{"resume": batchId}``): a run picks up where one stopped. Everything done is reused; what failed, was
|
|
12
|
+
blocked or was cancelled runs again, unless it may have acted: a step whose outcome is unknown, or one that failed
|
|
13
|
+
after acting outward (``acted``), is never run again and stays for you to review. ``preview`` says which is which.
|
|
14
|
+
- **Resume several** (``resume_all``): each of a list of runs, oldest first, one after another, on one thread.
|
|
15
|
+
|
|
16
|
+
A re-run is a new run with a new key: it never replays the earlier one's requests, and what it reuses it never acts on.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
from .. import harness_api
|
|
24
|
+
from ..api_errors import APIError
|
|
25
|
+
from .catalog import TRIGGERS
|
|
26
|
+
from .run import Debug
|
|
27
|
+
|
|
28
|
+
MAX_RESUMES = 20
|
|
29
|
+
RESUME_WAIT_S = 24 * 3600 # the longest a bulk resume waits for one of its runs before it starts the next
|
|
30
|
+
RUN_ID = re.compile(r"[a-f0-9]{12}")
|
|
31
|
+
|
|
32
|
+
# Kinds that never act outward: one that failed may run again on a resume.
|
|
33
|
+
SAFE_KINDS = frozenset({"transform", "branch", "merge", "output", "wait", "phone.check", "app.read", "loop", "brief",
|
|
34
|
+
"ai.ask"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def refuse(reason):
|
|
38
|
+
return APIError(reason, 409, "rerun_unavailable")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _source(owner, workflow_id, batch_id):
|
|
42
|
+
record = owner.batches.get(batch_id) if isinstance(batch_id, str) else None
|
|
43
|
+
if record is None or record["workflowId"] != workflow_id:
|
|
44
|
+
raise refuse("That run isn't kept any more. Mobster keeps the newest 20 runs of each workflow.")
|
|
45
|
+
if record.get("graph") != 2:
|
|
46
|
+
raise refuse("That run is from before workflows were graphs, so its steps can't be reused. Run it again.")
|
|
47
|
+
if record.get("status") == "running":
|
|
48
|
+
raise refuse("That run is still going. Wait for it to end.")
|
|
49
|
+
return record, owner.batches.nodes_with_io(batch_id)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _top(graph, ident):
|
|
53
|
+
nodes = {node["id"]: node for node in graph["nodes"]}
|
|
54
|
+
at = ident
|
|
55
|
+
while at in nodes and nodes[at].get("parent") is not None:
|
|
56
|
+
at = nodes[at]["parent"]
|
|
57
|
+
return at if at in nodes else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _lifted(graph):
|
|
61
|
+
"""The top level's edges, an edge into a loop's body counted as one into the loop."""
|
|
62
|
+
out = set()
|
|
63
|
+
for edge in graph.get("edges") or []:
|
|
64
|
+
a, b = _top(graph, edge["source"]), _top(graph, edge["target"])
|
|
65
|
+
if a and b and a != b:
|
|
66
|
+
out.add((a, b))
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _after(graph, ident):
|
|
71
|
+
edges, out, pending = _lifted(graph), set(), [ident]
|
|
72
|
+
while pending:
|
|
73
|
+
at = pending.pop()
|
|
74
|
+
for a, b in edges:
|
|
75
|
+
if a == at and b not in out:
|
|
76
|
+
out.add(b)
|
|
77
|
+
pending.append(b)
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _before(graph, ident):
|
|
82
|
+
edges, out, pending = _lifted(graph), set(), [ident]
|
|
83
|
+
while pending:
|
|
84
|
+
at = pending.pop()
|
|
85
|
+
for a, b in edges:
|
|
86
|
+
if b == at and a not in out:
|
|
87
|
+
out.add(a)
|
|
88
|
+
pending.append(a)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _recorded(entry, io):
|
|
93
|
+
io = io or {}
|
|
94
|
+
return {"state": entry["state"], "output": io.get("output"), "input": io.get("input"), "error": io.get("error"),
|
|
95
|
+
"handled": bool(entry.get("handled")),
|
|
96
|
+
"port": entry.get("port"), "reason": entry.get("reason"), "status": entry.get("status"),
|
|
97
|
+
"runId": entry.get("runId"), "answer": entry.get("answer"), "startedAt": entry.get("startedAt"),
|
|
98
|
+
"items": io.get("items")}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cut(io):
|
|
102
|
+
return any(isinstance(value, dict) and value.get("cut") for value in (io or {}).values())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _title(graph, ident):
|
|
106
|
+
node = next((n for n in graph["nodes"] if n["id"] == ident), None)
|
|
107
|
+
return f"“{(node or {}).get('title') or ident}”"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _entry_of(graph, rows):
|
|
111
|
+
trigger = next(((entry, io) for entry, io in rows if entry.get("type") in TRIGGERS and entry["state"] == "done"),
|
|
112
|
+
None)
|
|
113
|
+
if trigger is None:
|
|
114
|
+
raise refuse("That run's trigger isn't kept, so it can't be picked up. Run it again.")
|
|
115
|
+
entry, io = trigger
|
|
116
|
+
if entry["type"] not in {node["type"] for node in graph["nodes"]}:
|
|
117
|
+
raise refuse("The workflow no longer has the trigger that run started from. Run it again.")
|
|
118
|
+
payload = (io or {}).get("output") or {}
|
|
119
|
+
return entry["type"], payload, (payload.get("values") if isinstance(payload, dict) else None) or {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def plan(owner, workflow, graph, spec, *, test):
|
|
123
|
+
"""(Debug, entry, payload, values, source) for a re-run, or Debug(test) with no source for a plain test run."""
|
|
124
|
+
if not spec:
|
|
125
|
+
return Debug(test=test), None, None, None, None
|
|
126
|
+
if not isinstance(spec, dict) or len(spec) != 1 or next(iter(spec)) not in ("from", "only", "resume"):
|
|
127
|
+
raise ValueError("A re-run is from, only or resume")
|
|
128
|
+
kind, value = next(iter(spec.items()))
|
|
129
|
+
if kind == "resume":
|
|
130
|
+
return resume(owner, workflow, graph, value, test=test)
|
|
131
|
+
if not isinstance(value, dict) or set(value) - {"batchId", "node", "iteration", "input"}:
|
|
132
|
+
raise ValueError(f"{kind} takes batchId, node" + (", iteration and input" if kind == "from" else " and input"))
|
|
133
|
+
ident = value.get("node")
|
|
134
|
+
nodes = {node["id"]: node for node in graph["nodes"]}
|
|
135
|
+
if ident not in nodes:
|
|
136
|
+
raise ValueError("Name a step of this workflow")
|
|
137
|
+
batch_id = value.get("batchId")
|
|
138
|
+
if batch_id is None:
|
|
139
|
+
if kind == "from":
|
|
140
|
+
raise ValueError("Name the run to re-run from (batchId)")
|
|
141
|
+
recent = [r for r in owner.batches.recent(workflow["id"]) if r.get("graph") == 2 and r["status"] != "running"]
|
|
142
|
+
if not recent:
|
|
143
|
+
raise refuse("This workflow has no run to take the step's input from. Run it first.")
|
|
144
|
+
batch_id = recent[0]["id"]
|
|
145
|
+
record, rows = _source(owner, workflow["id"], batch_id)
|
|
146
|
+
iteration = value.get("iteration")
|
|
147
|
+
if nodes[ident].get("parent") is not None:
|
|
148
|
+
raise refuse("That step is inside a loop. Re-run the loop, or one round of it (iteration).")
|
|
149
|
+
debug = Debug(test=test)
|
|
150
|
+
after = _after(graph, ident)
|
|
151
|
+
if iteration is not None:
|
|
152
|
+
if nodes[ident]["type"] != "loop" or kind != "from" or type(iteration) is not int or iteration < 0:
|
|
153
|
+
raise ValueError("iteration is a round of a loop: a whole number from 0")
|
|
154
|
+
loop = next(((entry, io) for entry, io in rows if entry["instance"] == ident), None)
|
|
155
|
+
items = (loop[1] or {}).get("items") if loop else None
|
|
156
|
+
rounds = 1 + max((e["iteration"][0] for e, _io in rows if (e.get("iteration") or []) and
|
|
157
|
+
e["instance"].split("@")[0] in nodes and _top(graph, e["instance"].split("@")[0]) == ident),
|
|
158
|
+
default=-1)
|
|
159
|
+
if iteration >= max(rounds, len(items) if isinstance(items, list) else 0):
|
|
160
|
+
raise refuse(f"That run has no round {iteration + 1} of {_title(graph, ident)}.")
|
|
161
|
+
item = items[iteration] if isinstance(items, list) and iteration < len(items) else iteration
|
|
162
|
+
debug.items[ident] = (iteration, item)
|
|
163
|
+
if kind == "only" or iteration is not None:
|
|
164
|
+
what = "Only one round of" if iteration is not None else "Only"
|
|
165
|
+
for later in after:
|
|
166
|
+
debug.skip[later] = f"{what} {_title(graph, ident)} ran in this run."
|
|
167
|
+
rerun = {ident} | after # these run again, or are skipped: never reused
|
|
168
|
+
needed = _before(graph, ident)
|
|
169
|
+
by_node = {}
|
|
170
|
+
for entry, io in rows:
|
|
171
|
+
node_id = entry["instance"].split("@")[0]
|
|
172
|
+
top = _top(graph, node_id)
|
|
173
|
+
if top is None or nodes.get(node_id, {}).get("type") != entry.get("type"):
|
|
174
|
+
continue
|
|
175
|
+
if top in rerun:
|
|
176
|
+
continue
|
|
177
|
+
by_node.setdefault(top, []).append((entry, io))
|
|
178
|
+
if entry["state"] in ("done", "skipped") or entry["state"] == "failed" and entry.get("handled"):
|
|
179
|
+
if top in needed and _cut(io):
|
|
180
|
+
raise refuse(f"{_title(graph, top)}'s result was too big to keep, so it can't be reused. "
|
|
181
|
+
"Run the workflow from the start.")
|
|
182
|
+
debug.reuse[entry["instance"]] = _recorded(entry, io)
|
|
183
|
+
elif top in needed:
|
|
184
|
+
raise refuse(f"{_title(graph, top)} didn't finish in that run, so its result can't be reused. "
|
|
185
|
+
"Re-run from it instead.")
|
|
186
|
+
else:
|
|
187
|
+
debug.reuse[entry["instance"]] = _recorded(entry, io)
|
|
188
|
+
missing = [top for top in needed if top not in by_node]
|
|
189
|
+
if missing:
|
|
190
|
+
raise refuse(f"{_title(graph, missing[0])} didn't run in that run. Run the workflow from the start.")
|
|
191
|
+
if "input" in value:
|
|
192
|
+
debug.inputs[ident] = value["input"]
|
|
193
|
+
entry, payload, values = _entry_of(graph, rows)
|
|
194
|
+
return debug, entry, payload, values, {"batchId": batch_id, kind: ident,
|
|
195
|
+
**({"iteration": iteration} if iteration is not None else {})}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def resume(owner, workflow, graph, batch_id, *, test):
|
|
199
|
+
record, rows = _source(owner, workflow["id"], batch_id)
|
|
200
|
+
debug = Debug(test=test)
|
|
201
|
+
for entry, io in rows:
|
|
202
|
+
verdict, _reason = judge(graph, entry)
|
|
203
|
+
if verdict in ("reuse", "keep"): # kept as it ended: what depends on it stays stopped
|
|
204
|
+
debug.reuse[entry["instance"]] = _recorded(entry, io)
|
|
205
|
+
entry, payload, values = _entry_of(graph, rows)
|
|
206
|
+
return debug, entry, payload, values, {"batchId": batch_id, "resume": True}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def judge(graph, entry):
|
|
210
|
+
"""("reuse" | "run" | "keep", why); keep is an instance that may have acted, so what follows it stays stopped."""
|
|
211
|
+
node_id = entry["instance"].split("@")[0]
|
|
212
|
+
node = next((n for n in graph["nodes"] if n["id"] == node_id), None)
|
|
213
|
+
if node is None or node["type"] != entry.get("type"):
|
|
214
|
+
return "run", "It changed since that run."
|
|
215
|
+
state = entry["state"]
|
|
216
|
+
if state in ("done", "skipped") or state == "failed" and entry.get("handled"):
|
|
217
|
+
return "reuse", None
|
|
218
|
+
if state == "unknown":
|
|
219
|
+
return ("keep", "Its outcome is unknown, so Mobster won't run it again. Check it, then "
|
|
220
|
+
"re-run from the step after it.")
|
|
221
|
+
if state == "failed" and node["type"] not in SAFE_KINDS and entry.get("acted") is not False:
|
|
222
|
+
return ("keep", "It may have acted before it failed, so Mobster won't run it again. "
|
|
223
|
+
"Check it, then re-run from it.")
|
|
224
|
+
return "run", None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def preview(owner, workflow, graph, batch_id):
|
|
228
|
+
"""GET /api/workflows/{id}/runs/{batchId}/resume: what a resume would reuse, run again and leave."""
|
|
229
|
+
_record, rows = _source(owner, workflow["id"], batch_id)
|
|
230
|
+
out = {"reuse": [], "run": [], "skip": []}
|
|
231
|
+
for entry, _io in rows:
|
|
232
|
+
verdict, reason = judge(graph, entry)
|
|
233
|
+
if verdict == "keep":
|
|
234
|
+
out["skip"].append({"instance": entry["instance"], "reason": reason})
|
|
235
|
+
else:
|
|
236
|
+
out[verdict].append(entry["instance"])
|
|
237
|
+
return out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def recorded_scope(owner, workflow, batch_id, node):
|
|
241
|
+
"""The values an expression at ``node`` read in a past run, for POST /api/workflows/evaluate's preview."""
|
|
242
|
+
_record, rows = _source(owner, workflow["id"], batch_id)
|
|
243
|
+
done = {}
|
|
244
|
+
here = None
|
|
245
|
+
for entry, io in rows:
|
|
246
|
+
if entry["instance"] == node:
|
|
247
|
+
here = io or {}
|
|
248
|
+
if entry["state"] == "done" and "@" not in entry["instance"]:
|
|
249
|
+
done[entry["instance"]] = (io or {}).get("output")
|
|
250
|
+
trigger = next((io for entry, io in rows if entry.get("type") in TRIGGERS and entry["state"] == "done"), {}) or {}
|
|
251
|
+
payload = trigger.get("output") if isinstance(trigger.get("output"), dict) else {}
|
|
252
|
+
return {"input": (here or {}).get("input"), "inputs": payload.get("values") or {},
|
|
253
|
+
"vars": workflow["graph"].get("vars") or {}, "node": done,
|
|
254
|
+
"run": {"id": batch_id, "workflowId": workflow["id"], "trigger": None, "startedAt": None, "test": False}}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def resume_all(owner, identifier, batch_ids, request_key, origin=None):
|
|
258
|
+
if not isinstance(batch_ids, list) or not 1 <= len(batch_ids) <= MAX_RESUMES or len(set(batch_ids)) != len(
|
|
259
|
+
batch_ids) or not all(isinstance(b, str) and RUN_ID.fullmatch(b) for b in batch_ids):
|
|
260
|
+
raise ValueError(f"batchIds is a list of 1 to {MAX_RESUMES} different run ids")
|
|
261
|
+
if not isinstance(request_key, str) or not re.fullmatch(r"[A-Za-z0-9_.:-]{1,100}", request_key):
|
|
262
|
+
raise ValueError("Supply one valid Idempotency-Key to resume runs")
|
|
263
|
+
if origin in harness_api.UNATTENDED_ORIGINS:
|
|
264
|
+
raise APIError("A test run or a re-run starts from Mobster's editor.", 403, "test_from_editor")
|
|
265
|
+
with owner.journal.lock:
|
|
266
|
+
owner._get(identifier)
|
|
267
|
+
items = []
|
|
268
|
+
for batch_id in batch_ids:
|
|
269
|
+
record = owner.batches.get(batch_id)
|
|
270
|
+
try:
|
|
271
|
+
if record is None or record["workflowId"] != identifier:
|
|
272
|
+
raise APIError("That run isn't kept any more.", 404, "not_found")
|
|
273
|
+
if record.get("status") in ("running", "completed"):
|
|
274
|
+
raise APIError(f"It {record['status'].replace('_', ' ')}.", 409, "rerun_unavailable")
|
|
275
|
+
if not owner.resume_preview(identifier, batch_id)["run"]:
|
|
276
|
+
raise APIError("Nothing in it would run again.", 409, "rerun_unavailable")
|
|
277
|
+
items.append({"batchId": batch_id, "state": "queued", "createdAt": record.get("createdAt") or 0})
|
|
278
|
+
except APIError as error:
|
|
279
|
+
items.append({"batchId": batch_id, "state": "refused", "reason": str(error), "createdAt": None})
|
|
280
|
+
# Oldest first, as they ran; what is refused is listed after.
|
|
281
|
+
items.sort(key=lambda item: (item["createdAt"] is None, item["createdAt"] or 0))
|
|
282
|
+
for item in items:
|
|
283
|
+
item.pop("createdAt")
|
|
284
|
+
with owner._batches_lock:
|
|
285
|
+
if identifier in owner._resuming and owner._resuming[identifier].is_alive():
|
|
286
|
+
raise APIError("This workflow is already resuming a list of runs.", 409, "resume_busy")
|
|
287
|
+
thread = threading.Thread(target=_resume_each, args=(owner, identifier, items, request_key, origin),
|
|
288
|
+
name=f"mobster-resume-{identifier}", daemon=True)
|
|
289
|
+
owner._resuming[identifier] = thread
|
|
290
|
+
thread.start()
|
|
291
|
+
return {"resumes": [dict(item) for item in items]}
|
|
292
|
+
|
|
293
|
+
def _resume_each(owner, identifier, items, request_key, origin):
|
|
294
|
+
"""resume_all's thread: each queued run in turn, after the workflow's run before it has ended."""
|
|
295
|
+
def said():
|
|
296
|
+
_publish(owner, identifier, {"event": "resume_queue", "resumes": [dict(item) for item in items]})
|
|
297
|
+
for item in items:
|
|
298
|
+
if item["state"] != "queued":
|
|
299
|
+
continue
|
|
300
|
+
while identifier in owner.running() and not owner._closing.is_set():
|
|
301
|
+
owner._closing.wait(0.5)
|
|
302
|
+
if owner._closing.is_set():
|
|
303
|
+
item.update(state="refused", reason="Mobster quit before it started.")
|
|
304
|
+
break
|
|
305
|
+
try:
|
|
306
|
+
started = owner.start(identifier, f"{request_key}:{item['batchId']}"[:128],
|
|
307
|
+
rerun={"resume": item["batchId"]}, origin=origin)
|
|
308
|
+
except (APIError, ValueError) as error:
|
|
309
|
+
item.update(state="refused", reason=str(error)[:300])
|
|
310
|
+
said()
|
|
311
|
+
continue
|
|
312
|
+
item.update(state="running", resumedAs=(started.get("batch") or {}).get("id"))
|
|
313
|
+
said()
|
|
314
|
+
deadline = time.monotonic() + RESUME_WAIT_S
|
|
315
|
+
ended = {"status": "running"}
|
|
316
|
+
while ended["status"] in ("running", "queued") and time.monotonic() < deadline:
|
|
317
|
+
if owner._closing.is_set():
|
|
318
|
+
break
|
|
319
|
+
ended = owner.follow(identifier, started, 1.0)
|
|
320
|
+
item.update(state="ended", status=ended["status"])
|
|
321
|
+
said()
|
|
322
|
+
|
|
323
|
+
def _publish(owner, identifier, event):
|
|
324
|
+
publish = getattr(owner.runtime, "publish", None)
|
|
325
|
+
if callable(publish):
|
|
326
|
+
publish("workflows", {"workflowId": identifier, **event})
|
|
327
|
+
|