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,842 @@
|
|
|
1
|
+
"""A workflow graph's saved shape: its node types, their ports and validation (docs/workflows.mdx › The graph).
|
|
2
|
+
|
|
3
|
+
The node types and their fields are graph/catalog.py. A graph is ``{"version": 2, "inputs", "vars", "nodes", "edges",
|
|
4
|
+
"limits"}``. Each node has an ``id``, a ``type`` from
|
|
5
|
+
``TYPES``, its ``config``, a ``position`` the engine never reads, and may have a ``title``, ``notes``, a ``parent`` (the
|
|
6
|
+
loop it sits in) and a ``pin`` (test data, by output port). Each edge joins a source's output port to a target's input
|
|
7
|
+
port and may reshape the value on its way (``map``, an expression).
|
|
8
|
+
|
|
9
|
+
``validate(graph, context)`` returns the graph as saved (config defaults filled in) or raises ``GraphError`` listing
|
|
10
|
+
every problem with the node, edge, port or field it is about, so the canvas can mark each one. ``catalog()`` is what
|
|
11
|
+
``GET /api/workflows/node-types`` serves: every type with its fields and ports, so the canvas builds its palette and
|
|
12
|
+
inspector from the engine.
|
|
13
|
+
|
|
14
|
+
Screen text is data: a field that instructs a model (a phone step's words, a check's question, ``ai.ask``'s
|
|
15
|
+
instruction, a sub-workflow's values) may read only the person's own values (``$inputs``, ``$vars``, ``$run``, and a
|
|
16
|
+
loop's index, row or phone). Everything a step produced reaches a model only as fenced input.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Callable
|
|
22
|
+
|
|
23
|
+
from ..workflows import starters
|
|
24
|
+
from . import expr
|
|
25
|
+
from .catalog import (
|
|
26
|
+
ANY,
|
|
27
|
+
BOOLEAN,
|
|
28
|
+
DEFAULT_LIMITS,
|
|
29
|
+
ERROR_OUT,
|
|
30
|
+
ID,
|
|
31
|
+
INPUT_TYPES,
|
|
32
|
+
INTEGER,
|
|
33
|
+
MAX_EDGES,
|
|
34
|
+
MAX_FANOUT,
|
|
35
|
+
MAX_GRAPH_BYTES,
|
|
36
|
+
MAX_INPUTS,
|
|
37
|
+
MAX_ITERATIONS,
|
|
38
|
+
MAX_NESTING,
|
|
39
|
+
MAX_NODES,
|
|
40
|
+
MAX_PIN_BYTES,
|
|
41
|
+
MAX_PINS_BYTES,
|
|
42
|
+
MAX_VARS,
|
|
43
|
+
MAX_VARS_BYTES,
|
|
44
|
+
MINUTES_RANGE,
|
|
45
|
+
NOTES_CHARS,
|
|
46
|
+
NUMBER,
|
|
47
|
+
POSITION,
|
|
48
|
+
RETRY_BACKOFF,
|
|
49
|
+
RETRY_TRIES,
|
|
50
|
+
RETRY_TYPES,
|
|
51
|
+
SPEND_RANGE,
|
|
52
|
+
STRING,
|
|
53
|
+
TITLE_CHARS,
|
|
54
|
+
TRIGGERS,
|
|
55
|
+
TRUSTED_ITEMS,
|
|
56
|
+
TRUSTED_ROOTS,
|
|
57
|
+
TYPES,
|
|
58
|
+
VAR,
|
|
59
|
+
VERSION,
|
|
60
|
+
Field,
|
|
61
|
+
Shapes,
|
|
62
|
+
_Fields,
|
|
63
|
+
_json_size,
|
|
64
|
+
_short,
|
|
65
|
+
array,
|
|
66
|
+
catalog,
|
|
67
|
+
pointer,
|
|
68
|
+
public_limits,
|
|
69
|
+
register_node_type,
|
|
70
|
+
)
|
|
71
|
+
from .expr import ExprError
|
|
72
|
+
from .infer import RUN_SCHEMA, infer, schema_kind
|
|
73
|
+
|
|
74
|
+
__all__ = ["VERSION", "TYPES", "TRIGGERS", "DEFAULT_LIMITS", "ERROR_OUT", "MAX_ITERATIONS", "GraphError", "Context",
|
|
75
|
+
"validate", "check_graph", "coerce", "catalog", "public_limits", "register_node_type", "pointer"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GraphError(ValueError):
|
|
79
|
+
"""A graph that can't be saved."""
|
|
80
|
+
|
|
81
|
+
code = "invalid_graph"
|
|
82
|
+
|
|
83
|
+
def __init__(self, errors):
|
|
84
|
+
self.errors = list(errors)
|
|
85
|
+
more = len(self.errors) - 1
|
|
86
|
+
super().__init__(self.errors[0]["message"] + (f" (and {more} more)" if more else ""))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Context:
|
|
91
|
+
"""What validation asks the rest of Mobster."""
|
|
92
|
+
workflow_id: str | None = None
|
|
93
|
+
workflows: dict | None = None
|
|
94
|
+
app_ids: Callable | None = None
|
|
95
|
+
device: Callable | None = None
|
|
96
|
+
account: Callable | None = None
|
|
97
|
+
destination: Callable | None = None
|
|
98
|
+
connector: Callable | None = None
|
|
99
|
+
packs: dict | None = None
|
|
100
|
+
cron: Callable | None = None
|
|
101
|
+
timezone: Callable | None = None
|
|
102
|
+
deliveries: tuple = ("notification", "clipboard", "file", "destination")
|
|
103
|
+
plain: Callable | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _Validator:
|
|
107
|
+
def __init__(self, graph, ctx):
|
|
108
|
+
self.graph, self.context = graph, ctx
|
|
109
|
+
self.problems = []
|
|
110
|
+
self.nodes = {}
|
|
111
|
+
self.edges = []
|
|
112
|
+
self.expressions = []
|
|
113
|
+
self.field_types = {}
|
|
114
|
+
self._schemas = {}
|
|
115
|
+
self._input = {}
|
|
116
|
+
self._busy = set()
|
|
117
|
+
|
|
118
|
+
def problem(self, message, **where):
|
|
119
|
+
self.problems.append({**{k: v for k, v in where.items() if v is not None}, "message": str(message)})
|
|
120
|
+
|
|
121
|
+
def run(self):
|
|
122
|
+
graph = self.graph
|
|
123
|
+
if not isinstance(graph, dict):
|
|
124
|
+
raise GraphError([{"message": "A graph is one JSON object"}])
|
|
125
|
+
extra = set(graph) - {"version", "inputs", "vars", "nodes", "edges", "limits"}
|
|
126
|
+
if extra:
|
|
127
|
+
self.problem("A graph has only version, inputs, vars, nodes, edges and limits, not "
|
|
128
|
+
+ ", ".join(sorted(extra)))
|
|
129
|
+
if graph.get("version") != VERSION:
|
|
130
|
+
self.problem(f"This is a graph of version {VERSION}")
|
|
131
|
+
try:
|
|
132
|
+
size = _json_size(graph)
|
|
133
|
+
except (TypeError, ValueError):
|
|
134
|
+
raise GraphError([{"message": "A graph is plain JSON"}]) from None
|
|
135
|
+
if size > MAX_GRAPH_BYTES:
|
|
136
|
+
raise GraphError([{"message": f"A graph is at most {MAX_GRAPH_BYTES // 1000} KB, pins included"}])
|
|
137
|
+
inputs = self.inputs(graph.get("inputs"))
|
|
138
|
+
variables = self.variables(graph.get("vars"))
|
|
139
|
+
limits = self.limits(graph.get("limits"))
|
|
140
|
+
nodes = self.read_nodes(graph.get("nodes"))
|
|
141
|
+
self.read_parents()
|
|
142
|
+
self.read_edges(graph.get("edges"))
|
|
143
|
+
if not self.problems:
|
|
144
|
+
self.structure()
|
|
145
|
+
if not self.problems:
|
|
146
|
+
self.values_schema = self.make_values_schema(inputs)
|
|
147
|
+
self.vars_schema = {"type": "object", "properties": {k: _schema_of_value(x) for k, x in variables.items()},
|
|
148
|
+
"additionalProperties": False}
|
|
149
|
+
self.check_expressions()
|
|
150
|
+
self.check_pins()
|
|
151
|
+
if self.problems:
|
|
152
|
+
raise GraphError(self.problems)
|
|
153
|
+
return {"version": VERSION, "inputs": inputs, "vars": variables, "nodes": nodes,
|
|
154
|
+
"edges": [edge for edge in self.edges], "limits": limits}
|
|
155
|
+
|
|
156
|
+
def inputs(self, value):
|
|
157
|
+
if value is None:
|
|
158
|
+
return []
|
|
159
|
+
if not isinstance(value, list) or len(value) > MAX_INPUTS:
|
|
160
|
+
self.problem(f"A graph has at most {MAX_INPUTS} inputs", field="inputs")
|
|
161
|
+
return []
|
|
162
|
+
out, seen = [], set()
|
|
163
|
+
for item in value:
|
|
164
|
+
try:
|
|
165
|
+
if not isinstance(item, dict) or set(item) - {"name", "type", "default", "description", "required"}:
|
|
166
|
+
raise ValueError("An input has a name, a type, and may have a default, a description and required")
|
|
167
|
+
name = _short(item.get("name"), "An input's name", starters.NAME_CHARS)
|
|
168
|
+
if not starters.NAMED.fullmatch(name):
|
|
169
|
+
raise ValueError(f"{name} can't be an input's name: start with a letter")
|
|
170
|
+
key = starters._key(name)
|
|
171
|
+
if key in seen:
|
|
172
|
+
raise ValueError(f"Two inputs are called {name}")
|
|
173
|
+
seen.add(key)
|
|
174
|
+
kind = item.get("type", "string")
|
|
175
|
+
if kind not in INPUT_TYPES:
|
|
176
|
+
raise ValueError("An input's type is " + ", ".join(INPUT_TYPES))
|
|
177
|
+
default = item.get("default")
|
|
178
|
+
if default is not None:
|
|
179
|
+
default = coerce(default, kind, name)
|
|
180
|
+
cleaned = {"name": name, "type": kind}
|
|
181
|
+
if default is not None:
|
|
182
|
+
cleaned["default"] = default
|
|
183
|
+
if item.get("description"):
|
|
184
|
+
cleaned["description"] = _short(item["description"], "A description", 200)
|
|
185
|
+
if item.get("required") is not None:
|
|
186
|
+
if type(item["required"]) is not bool:
|
|
187
|
+
raise ValueError("required is true or false")
|
|
188
|
+
cleaned["required"] = item["required"]
|
|
189
|
+
out.append(cleaned)
|
|
190
|
+
except ValueError as error:
|
|
191
|
+
self.problem(error, field="inputs")
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
def variables(self, value):
|
|
195
|
+
if value is None:
|
|
196
|
+
return {}
|
|
197
|
+
if not isinstance(value, dict) or len(value) > MAX_VARS:
|
|
198
|
+
self.problem(f"A graph has at most {MAX_VARS} vars", field="vars")
|
|
199
|
+
return {}
|
|
200
|
+
for name in value:
|
|
201
|
+
if not isinstance(name, str) or not VAR.fullmatch(name):
|
|
202
|
+
self.problem(f"A var's name is letters, digits and _, starting with a letter: {str(name)[:40]!r}",
|
|
203
|
+
field="vars")
|
|
204
|
+
if _json_size(value) > MAX_VARS_BYTES:
|
|
205
|
+
self.problem(f"A graph's vars are at most {MAX_VARS_BYTES // 1000} KB in all", field="vars")
|
|
206
|
+
return dict(value)
|
|
207
|
+
|
|
208
|
+
def limits(self, value):
|
|
209
|
+
value = value if value is not None else {}
|
|
210
|
+
if not isinstance(value, dict) or set(value) - set(DEFAULT_LIMITS):
|
|
211
|
+
self.problem("A workflow's limits are maxSpendUsd and maxMinutes", field="limits")
|
|
212
|
+
return dict(DEFAULT_LIMITS)
|
|
213
|
+
spend = value.get("maxSpendUsd", DEFAULT_LIMITS["maxSpendUsd"])
|
|
214
|
+
if (isinstance(spend, bool) or not isinstance(spend, (int, float))
|
|
215
|
+
or not SPEND_RANGE[0] <= spend <= SPEND_RANGE[1]):
|
|
216
|
+
self.problem(f"maxSpendUsd is from ${SPEND_RANGE[0]:.2f} to ${SPEND_RANGE[1]:.0f}", field="limits")
|
|
217
|
+
spend = DEFAULT_LIMITS["maxSpendUsd"]
|
|
218
|
+
minutes = value.get("maxMinutes", DEFAULT_LIMITS["maxMinutes"])
|
|
219
|
+
if type(minutes) is not int or not MINUTES_RANGE[0] <= minutes <= MINUTES_RANGE[1]:
|
|
220
|
+
self.problem(f"maxMinutes is a whole number from {MINUTES_RANGE[0]} to {MINUTES_RANGE[1]:,}",
|
|
221
|
+
field="limits")
|
|
222
|
+
minutes = DEFAULT_LIMITS["maxMinutes"]
|
|
223
|
+
return {"maxSpendUsd": round(float(spend), 2), "maxMinutes": minutes}
|
|
224
|
+
|
|
225
|
+
def read_nodes(self, value):
|
|
226
|
+
if not isinstance(value, list) or not value:
|
|
227
|
+
self.problem("A graph has at least one node", field="nodes")
|
|
228
|
+
return []
|
|
229
|
+
if len(value) > MAX_NODES:
|
|
230
|
+
self.problem(f"A graph has at most {MAX_NODES} nodes", field="nodes")
|
|
231
|
+
return []
|
|
232
|
+
out = []
|
|
233
|
+
for raw in value:
|
|
234
|
+
node = self.read_node(raw)
|
|
235
|
+
if node is not None:
|
|
236
|
+
out.append(node)
|
|
237
|
+
kinds = [node["type"] for node in out if node["type"] in TRIGGERS]
|
|
238
|
+
if not kinds:
|
|
239
|
+
self.problem("Add a trigger: when the workflow runs")
|
|
240
|
+
for kind in set(kinds):
|
|
241
|
+
if kinds.count(kind) > 1:
|
|
242
|
+
self.problem(f"A graph has one {TYPES[kind].label} trigger at most")
|
|
243
|
+
if sum(1 for node in out if node["type"] == "output") > 1:
|
|
244
|
+
self.problem("A graph has one Result at most")
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
def read_node(self, raw):
|
|
248
|
+
if not isinstance(raw, dict):
|
|
249
|
+
self.problem("Each node is an object")
|
|
250
|
+
return None
|
|
251
|
+
ident = raw.get("id")
|
|
252
|
+
if not isinstance(ident, str) or not ID.fullmatch(ident):
|
|
253
|
+
self.problem("Each node has an id: 1 to 24 letters, digits, - or _")
|
|
254
|
+
return None
|
|
255
|
+
if ident in self.nodes:
|
|
256
|
+
self.problem(f"Two nodes have the id {ident}", node=ident)
|
|
257
|
+
return None
|
|
258
|
+
extra = set(raw) - {"id", "type", "config", "position", "title", "parent", "pin", "notes", "retry"}
|
|
259
|
+
if extra:
|
|
260
|
+
self.problem("A node has only id, type, config, position, title, parent, pin, notes and retry", node=ident)
|
|
261
|
+
kind = raw.get("type")
|
|
262
|
+
node_type = TYPES.get(kind)
|
|
263
|
+
if node_type is None:
|
|
264
|
+
self.problem(f"Mobster has no node of type {str(kind)[:40]!r}", node=ident)
|
|
265
|
+
return None
|
|
266
|
+
node = {"id": ident, "type": kind, "config": {}, "position": self.position(raw.get("position"), ident)}
|
|
267
|
+
for key, high in (("title", TITLE_CHARS), ("notes", NOTES_CHARS)):
|
|
268
|
+
if raw.get(key) is not None:
|
|
269
|
+
try:
|
|
270
|
+
node[key] = _short(raw[key], "A node's " + key, high)
|
|
271
|
+
except ValueError as error:
|
|
272
|
+
self.problem(error, node=ident, field=key)
|
|
273
|
+
if raw.get("parent") is not None:
|
|
274
|
+
node["parent"] = raw["parent"]
|
|
275
|
+
if raw.get("pin") is not None:
|
|
276
|
+
node["pin"] = raw["pin"]
|
|
277
|
+
if raw.get("retry") is not None:
|
|
278
|
+
retry = self.retry(raw["retry"], kind, ident)
|
|
279
|
+
if retry is not None:
|
|
280
|
+
node["retry"] = retry
|
|
281
|
+
self.nodes[ident] = node
|
|
282
|
+
node["config"] = self.config(node, node_type, raw.get("config"))
|
|
283
|
+
return node
|
|
284
|
+
|
|
285
|
+
def retry(self, value, kind, ident):
|
|
286
|
+
if kind not in RETRY_TYPES:
|
|
287
|
+
self.problem(f"A {TYPES[kind].label} step can act outward, so Mobster never retries it", node=ident,
|
|
288
|
+
field="retry")
|
|
289
|
+
return None
|
|
290
|
+
if (not isinstance(value, dict) or set(value) - {"tries", "backoffSeconds"}
|
|
291
|
+
or type(value.get("tries")) is not int or not RETRY_TRIES[0] <= value["tries"] <= RETRY_TRIES[1]
|
|
292
|
+
or type(value.get("backoffSeconds", 30)) is not int
|
|
293
|
+
or not RETRY_BACKOFF[0] <= value.get("backoffSeconds", 30) <= RETRY_BACKOFF[1]):
|
|
294
|
+
self.problem(f"retry is {{tries: {RETRY_TRIES[0]} to {RETRY_TRIES[1]}, backoffSeconds: {RETRY_BACKOFF[0]} "
|
|
295
|
+
f"to {RETRY_BACKOFF[1]:,}}}", node=ident, field="retry")
|
|
296
|
+
return None
|
|
297
|
+
return {"tries": value["tries"], "backoffSeconds": value.get("backoffSeconds", 30)}
|
|
298
|
+
|
|
299
|
+
def position(self, value, ident):
|
|
300
|
+
if value is None:
|
|
301
|
+
return {"x": 0, "y": 0}
|
|
302
|
+
if (not isinstance(value, dict) or set(value) != {"x", "y"} or
|
|
303
|
+
any(isinstance(n, bool) or not isinstance(n, (int, float)) or not math.isfinite(n)
|
|
304
|
+
or abs(n) > POSITION for n in value.values())):
|
|
305
|
+
self.problem(f"A node's position is {{x, y}}, each within ±{POSITION:,}", node=ident, field="position")
|
|
306
|
+
return {"x": 0, "y": 0}
|
|
307
|
+
return {"x": value["x"], "y": value["y"]}
|
|
308
|
+
|
|
309
|
+
def config(self, node, node_type, value):
|
|
310
|
+
ident = node["id"]
|
|
311
|
+
value = value if value is not None else {}
|
|
312
|
+
if not isinstance(value, dict):
|
|
313
|
+
self.problem("A node's config is one object", node=ident, field="config")
|
|
314
|
+
return {}
|
|
315
|
+
for key in set(value) - set(node_type.fields):
|
|
316
|
+
self.problem(f"A {node_type.label} node has no field {key}", node=ident, field=key)
|
|
317
|
+
out = {}
|
|
318
|
+
node["config"] = out
|
|
319
|
+
failed = False
|
|
320
|
+
for name, f in node_type.fields.items():
|
|
321
|
+
given = value.get(name, f.default)
|
|
322
|
+
if given is None:
|
|
323
|
+
if f.required:
|
|
324
|
+
self.problem(f"{f.label} is needed", node=ident, field=name)
|
|
325
|
+
failed = True
|
|
326
|
+
out[name] = None
|
|
327
|
+
continue
|
|
328
|
+
try:
|
|
329
|
+
out[name] = getattr(_Fields, f.kind)(given, f, self.context, self, node)
|
|
330
|
+
except (ValueError, ExprError) as error:
|
|
331
|
+
self.problem(error, node=ident, field=name)
|
|
332
|
+
failed = True
|
|
333
|
+
out[name] = None
|
|
334
|
+
if failed:
|
|
335
|
+
return out
|
|
336
|
+
if node_type.check is not None:
|
|
337
|
+
try:
|
|
338
|
+
out.update(node_type.check(dict(out), self.context))
|
|
339
|
+
except (ValueError, ExprError) as error:
|
|
340
|
+
self.problem(error, node=ident)
|
|
341
|
+
return out
|
|
342
|
+
self.collect_expressions(node, node_type)
|
|
343
|
+
return out
|
|
344
|
+
|
|
345
|
+
def collect_expressions(self, node, node_type):
|
|
346
|
+
for name, f in node_type.fields.items():
|
|
347
|
+
if not f.expr or node["config"].get(name) is None:
|
|
348
|
+
continue
|
|
349
|
+
value = node["config"][name]
|
|
350
|
+
if f.expr == "bare":
|
|
351
|
+
self.expressions.append((node["id"], (name,), expr.compile(value), f.instructs, f.wants))
|
|
352
|
+
continue
|
|
353
|
+
for path, raw in _strings(value, (name,)) if f.expr == "deep" else [((name,), value)]:
|
|
354
|
+
try:
|
|
355
|
+
shape = expr.template(raw)
|
|
356
|
+
except ExprError as error:
|
|
357
|
+
self.problem(f"{error}", node=node["id"], field=".".join(map(str, path)))
|
|
358
|
+
continue
|
|
359
|
+
for parsed in shape.expressions:
|
|
360
|
+
self.expressions.append((node["id"], path, parsed, f.instructs, None))
|
|
361
|
+
if shape.whole is not None:
|
|
362
|
+
self.expressions.append((node["id"], path + ("whole",), shape.whole, False, None))
|
|
363
|
+
if node["type"] == "branch":
|
|
364
|
+
for case in node["config"].get("cases") or []:
|
|
365
|
+
self.expressions.append((node["id"], ("cases", case["id"]), expr.compile(case["when"]), False,
|
|
366
|
+
"boolean"))
|
|
367
|
+
|
|
368
|
+
def read_parents(self):
|
|
369
|
+
for node in list(self.nodes.values()):
|
|
370
|
+
parent = node.get("parent")
|
|
371
|
+
if parent is None:
|
|
372
|
+
continue
|
|
373
|
+
owner = self.nodes.get(parent) if isinstance(parent, str) else None
|
|
374
|
+
if owner is None or owner["type"] != "loop":
|
|
375
|
+
self.problem("A node sits inside a loop: its parent is a loop's id", node=node["id"], field="parent")
|
|
376
|
+
node.pop("parent", None)
|
|
377
|
+
continue
|
|
378
|
+
if node["type"] in TRIGGERS or node["type"] == "output":
|
|
379
|
+
self.problem(f"A {TYPES[node['type']].label} can't sit inside a loop", node=node["id"],
|
|
380
|
+
field="parent")
|
|
381
|
+
for node in self.nodes.values():
|
|
382
|
+
seen, at = {node["id"]}, node.get("parent")
|
|
383
|
+
depth = 0
|
|
384
|
+
while at is not None:
|
|
385
|
+
if at in seen:
|
|
386
|
+
self.problem("Loops can't sit inside each other in a circle", node=node["id"], field="parent")
|
|
387
|
+
node.pop("parent", None)
|
|
388
|
+
break
|
|
389
|
+
seen.add(at)
|
|
390
|
+
depth += 1
|
|
391
|
+
at = self.nodes[at].get("parent")
|
|
392
|
+
if node["type"] == "loop" and depth >= MAX_NESTING:
|
|
393
|
+
self.problem(f"Loops sit inside each other at most {MAX_NESTING} deep", node=node["id"])
|
|
394
|
+
|
|
395
|
+
def scopes(self, ident):
|
|
396
|
+
"""The loops around a node, innermost first, ending with None (the top)."""
|
|
397
|
+
out, at = [], self.nodes[ident].get("parent")
|
|
398
|
+
while at is not None:
|
|
399
|
+
out.append(at)
|
|
400
|
+
at = self.nodes[at].get("parent")
|
|
401
|
+
return out + [None]
|
|
402
|
+
|
|
403
|
+
def lift(self, ident, level):
|
|
404
|
+
"""The node at ``level`` (a loop's id, or None for the top) that holds ``ident``, or None."""
|
|
405
|
+
at = ident
|
|
406
|
+
while at is not None:
|
|
407
|
+
if self.nodes[at].get("parent") == level:
|
|
408
|
+
return at
|
|
409
|
+
at = self.nodes[at].get("parent")
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
def outputs(self, ident):
|
|
413
|
+
node = self.nodes[ident]
|
|
414
|
+
node_type = TYPES[node["type"]]
|
|
415
|
+
ports = set(node_type.static)
|
|
416
|
+
if node["type"] == "branch":
|
|
417
|
+
ports |= {case["id"] for case in node["config"].get("cases") or []} | {"else"}
|
|
418
|
+
elif node["type"] == "loop":
|
|
419
|
+
ports |= {"item", "index", "out"}
|
|
420
|
+
elif node["type"] == "approval":
|
|
421
|
+
ports |= {"approved", "declined"}
|
|
422
|
+
elif node["type"] in ("merge", "wait", "workflow.call") or not ports:
|
|
423
|
+
ports |= {"out"} if node["type"] != "output" else set()
|
|
424
|
+
if not node_type.trigger:
|
|
425
|
+
ports.add("error")
|
|
426
|
+
return ports
|
|
427
|
+
|
|
428
|
+
def read_edges(self, value):
|
|
429
|
+
value = value if value is not None else []
|
|
430
|
+
if not isinstance(value, list) or len(value) > MAX_EDGES:
|
|
431
|
+
self.problem(f"A graph has at most {MAX_EDGES} edges", field="edges")
|
|
432
|
+
return
|
|
433
|
+
seen, pairs, fanout = set(), set(), {}
|
|
434
|
+
for raw in value:
|
|
435
|
+
if not isinstance(raw, dict):
|
|
436
|
+
self.problem("Each edge is an object")
|
|
437
|
+
continue
|
|
438
|
+
ident = raw.get("id")
|
|
439
|
+
if not isinstance(ident, str) or not ID.fullmatch(ident) or ident in seen:
|
|
440
|
+
self.problem("Each edge has its own id: 1 to 24 letters, digits, - or _")
|
|
441
|
+
continue
|
|
442
|
+
seen.add(ident)
|
|
443
|
+
extra = set(raw) - {"id", "source", "sourcePort", "target", "targetPort", "map"}
|
|
444
|
+
if extra:
|
|
445
|
+
self.problem("An edge has only id, source, sourcePort, target, targetPort and map", edge=ident)
|
|
446
|
+
source, target = raw.get("source"), raw.get("target")
|
|
447
|
+
sport, tport = raw.get("sourcePort", "out"), raw.get("targetPort", "in")
|
|
448
|
+
if source not in self.nodes or target not in self.nodes:
|
|
449
|
+
self.problem("An edge joins two nodes of this graph", edge=ident)
|
|
450
|
+
continue
|
|
451
|
+
if source == target:
|
|
452
|
+
self.problem("A node can't feed itself", edge=ident, node=source)
|
|
453
|
+
continue
|
|
454
|
+
if sport not in self.outputs(source):
|
|
455
|
+
self.problem(f"{self.name(source)} has no output called {sport}", edge=ident, node=source,
|
|
456
|
+
port=sport)
|
|
457
|
+
continue
|
|
458
|
+
target_type = TYPES[self.nodes[target]["type"]]
|
|
459
|
+
if target_type.trigger:
|
|
460
|
+
self.problem("Nothing runs into a trigger", edge=ident, node=target)
|
|
461
|
+
continue
|
|
462
|
+
if tport not in target_type.inputs:
|
|
463
|
+
self.problem(f"{self.name(target)} has no input called {tport}", edge=ident, node=target, port=tport)
|
|
464
|
+
continue
|
|
465
|
+
if (source, sport, target, tport) in pairs:
|
|
466
|
+
self.problem("Those two ports are joined twice", edge=ident)
|
|
467
|
+
continue
|
|
468
|
+
pairs.add((source, sport, target, tport))
|
|
469
|
+
fanout[(source, sport)] = fanout.get((source, sport), 0) + 1
|
|
470
|
+
if fanout[(source, sport)] == MAX_FANOUT + 1:
|
|
471
|
+
self.problem(f"One output feeds at most {MAX_FANOUT} steps", node=source, port=sport)
|
|
472
|
+
edge = {"id": ident, "source": source, "sourcePort": sport, "target": target, "targetPort": tport}
|
|
473
|
+
if raw.get("map") is not None:
|
|
474
|
+
try:
|
|
475
|
+
edge["map"] = _Fields.bare(raw["map"], Field("bare", "The edge's map"), self.context, self, None)
|
|
476
|
+
self.expressions.append(((ident, "edge"), ("map",), expr.compile(edge["map"]), False, None))
|
|
477
|
+
except (ValueError, ExprError) as error:
|
|
478
|
+
self.problem(error, edge=ident, field="map")
|
|
479
|
+
continue
|
|
480
|
+
if not self.scope_ok(edge):
|
|
481
|
+
continue
|
|
482
|
+
self.edges.append(edge)
|
|
483
|
+
for node in self.nodes.values():
|
|
484
|
+
node_type = TYPES[node["type"]]
|
|
485
|
+
for port in node_type.single:
|
|
486
|
+
count = sum(1 for e in self.edges if e["target"] == node["id"] and e["targetPort"] == port)
|
|
487
|
+
if count > 1:
|
|
488
|
+
self.problem(f"{self.name(node['id'])} takes one edge into {port}", node=node["id"], port=port)
|
|
489
|
+
|
|
490
|
+
def scope_ok(self, edge):
|
|
491
|
+
source, target, port = edge["source"], edge["target"], edge["sourcePort"]
|
|
492
|
+
inside = source in self.scopes(target)[:-1]
|
|
493
|
+
if self.nodes[source]["type"] == "loop" and port in ("item", "index"):
|
|
494
|
+
if not inside:
|
|
495
|
+
self.problem(f"A loop's {port} goes only to steps inside the loop", edge=edge["id"], node=source)
|
|
496
|
+
return False
|
|
497
|
+
return True
|
|
498
|
+
if inside:
|
|
499
|
+
self.problem("A loop's results leave through its out port, not into its own steps", edge=edge["id"],
|
|
500
|
+
node=source)
|
|
501
|
+
return False
|
|
502
|
+
if self.nodes[source].get("parent") not in self.scopes(target):
|
|
503
|
+
self.problem("A step inside a loop sends its results out through the loop's collect and out port",
|
|
504
|
+
edge=edge["id"], node=source)
|
|
505
|
+
return False
|
|
506
|
+
return True
|
|
507
|
+
|
|
508
|
+
def name(self, ident):
|
|
509
|
+
node = self.nodes[ident]
|
|
510
|
+
return f"“{node.get('title') or TYPES[node['type']].label}”"
|
|
511
|
+
|
|
512
|
+
def level_edges(self, level):
|
|
513
|
+
out = set()
|
|
514
|
+
for edge in self.edges:
|
|
515
|
+
if self.nodes[edge["source"]]["type"] == "loop" and edge["sourcePort"] in ("item", "index") \
|
|
516
|
+
and edge["source"] in self.scopes(edge["target"])[:-1]:
|
|
517
|
+
continue
|
|
518
|
+
a, b = self.lift(edge["source"], level), self.lift(edge["target"], level)
|
|
519
|
+
if a is not None and b is not None and a != b:
|
|
520
|
+
out.add((a, b))
|
|
521
|
+
return out
|
|
522
|
+
|
|
523
|
+
def levels(self):
|
|
524
|
+
return [None] + [ident for ident, node in self.nodes.items() if node["type"] == "loop"]
|
|
525
|
+
|
|
526
|
+
def structure(self):
|
|
527
|
+
self.order = {}
|
|
528
|
+
for level in self.levels():
|
|
529
|
+
members = [ident for ident, node in self.nodes.items() if node.get("parent") == level]
|
|
530
|
+
edges = self.level_edges(level)
|
|
531
|
+
order = _topological(members, edges)
|
|
532
|
+
if order is None:
|
|
533
|
+
cycle = _cycle(members, edges)
|
|
534
|
+
self.problem("These steps wait for each other in a circle: " + ", ".join(self.name(i) for i in cycle),
|
|
535
|
+
node=cycle[0])
|
|
536
|
+
return
|
|
537
|
+
self.order[level] = order
|
|
538
|
+
top = [ident for ident, node in self.nodes.items() if node.get("parent") is None]
|
|
539
|
+
reached = {ident for ident in top if self.nodes[ident]["type"] in TRIGGERS}
|
|
540
|
+
edges = self.level_edges(None)
|
|
541
|
+
changed = True
|
|
542
|
+
while changed:
|
|
543
|
+
changed = False
|
|
544
|
+
for a, b in edges:
|
|
545
|
+
if a in reached and b not in reached:
|
|
546
|
+
reached.add(b)
|
|
547
|
+
changed = True
|
|
548
|
+
for ident in top:
|
|
549
|
+
if ident not in reached:
|
|
550
|
+
self.problem(f"{self.name(ident)} never runs: no trigger leads to it", node=ident)
|
|
551
|
+
for node in self.nodes.values():
|
|
552
|
+
self.node_rules(node)
|
|
553
|
+
|
|
554
|
+
def incoming(self, ident, port=None):
|
|
555
|
+
return [e for e in self.edges if e["target"] == ident and (port is None or e["targetPort"] == port)]
|
|
556
|
+
|
|
557
|
+
def node_rules(self, node):
|
|
558
|
+
ident, config, kind = node["id"], node["config"], node["type"]
|
|
559
|
+
if kind == "merge" and len(self.incoming(ident)) < 2:
|
|
560
|
+
self.problem("A merge joins two or more paths", node=ident)
|
|
561
|
+
if kind == "branch" and len(self.incoming(ident, "in")) != 1:
|
|
562
|
+
self.problem("A branch takes exactly one edge", node=ident)
|
|
563
|
+
if kind == "loop":
|
|
564
|
+
items = self.incoming(ident, "items")
|
|
565
|
+
if config.get("over") == "items" and not items:
|
|
566
|
+
self.problem("Join a list to the loop's items", node=ident, port="items")
|
|
567
|
+
if config.get("over") != "items" and items:
|
|
568
|
+
self.problem("Only a loop over items takes the items input", node=ident, port="items")
|
|
569
|
+
for key in ("until", "collect"):
|
|
570
|
+
inner = config.get(key)
|
|
571
|
+
if inner is not None and (inner not in self.nodes or self.nodes[inner].get("parent") != ident):
|
|
572
|
+
self.problem(f"The loop's {key} is a step inside it", node=ident, field=key)
|
|
573
|
+
if config.get("over") == "until" and config.get("until") in self.nodes:
|
|
574
|
+
watched = self.nodes[config["until"]]
|
|
575
|
+
if TYPES[watched["type"]].trigger:
|
|
576
|
+
self.problem("The loop's until is a step inside it", node=ident, field="until")
|
|
577
|
+
if config.get("device") == "loop":
|
|
578
|
+
loop = next((self.nodes[s] for s in self.scopes(ident)[:-1]), None)
|
|
579
|
+
if loop is None or loop["config"].get("over") != "phones":
|
|
580
|
+
self.problem('A step runs on "loop" only inside a loop over phones', node=ident, field="device")
|
|
581
|
+
|
|
582
|
+
def make_values_schema(self, inputs):
|
|
583
|
+
kinds = {"string": STRING, "number": NUMBER, "boolean": BOOLEAN, "list": array(), "object": {"type": "object"}}
|
|
584
|
+
properties = {starters._key(item["name"]): kinds[item["type"]] for item in inputs}
|
|
585
|
+
for node in self.nodes.values():
|
|
586
|
+
for name, f in TYPES[node["type"]].fields.items():
|
|
587
|
+
if not f.blanks or node["config"].get(name) is None:
|
|
588
|
+
continue
|
|
589
|
+
for _path, raw in _strings(node["config"][name], ()):
|
|
590
|
+
for blank in expr.template(raw).blanks:
|
|
591
|
+
properties.setdefault(starters._key(blank[1]), STRING)
|
|
592
|
+
return {"type": "object", "properties": properties, "additionalProperties": False}
|
|
593
|
+
|
|
594
|
+
def schema_of(self, ident, port="out"):
|
|
595
|
+
key = (ident, port)
|
|
596
|
+
if key in self._schemas:
|
|
597
|
+
return self._schemas[key]
|
|
598
|
+
if port == "error":
|
|
599
|
+
return ERROR_OUT
|
|
600
|
+
if key in self._busy:
|
|
601
|
+
return ANY
|
|
602
|
+
self._busy.add(key)
|
|
603
|
+
try:
|
|
604
|
+
node = self.nodes[ident]
|
|
605
|
+
ports = TYPES[node["type"]].outputs(node, Shapes(self, node))
|
|
606
|
+
except (ValueError, ExprError, KeyError):
|
|
607
|
+
ports = {}
|
|
608
|
+
finally:
|
|
609
|
+
self._busy.discard(key)
|
|
610
|
+
for name, schema in ports.items():
|
|
611
|
+
self._schemas[(ident, name)] = schema if isinstance(schema, dict) else ANY
|
|
612
|
+
return self._schemas.get(key, ANY)
|
|
613
|
+
|
|
614
|
+
def edge_schema(self, edge):
|
|
615
|
+
schema = self.schema_of(edge["source"], edge["sourcePort"])
|
|
616
|
+
if edge.get("map"):
|
|
617
|
+
env = self.env_for_edge(edge, schema)
|
|
618
|
+
try:
|
|
619
|
+
schema = infer(edge["map"], env)
|
|
620
|
+
except ExprError:
|
|
621
|
+
return ANY
|
|
622
|
+
return schema
|
|
623
|
+
|
|
624
|
+
def input_schema(self, ident):
|
|
625
|
+
if ident in self._input:
|
|
626
|
+
return self._input[ident]
|
|
627
|
+
self._input[ident] = ANY
|
|
628
|
+
edges = self.incoming(ident, "in")
|
|
629
|
+
schemas = [self.edge_schema(edge) for edge in edges]
|
|
630
|
+
schema = array() if len(schemas) > 1 else schemas[0] if schemas else ANY
|
|
631
|
+
self._input[ident] = schema
|
|
632
|
+
return schema
|
|
633
|
+
|
|
634
|
+
def items_schema(self, ident):
|
|
635
|
+
edges = self.incoming(ident, "items")
|
|
636
|
+
return self.edge_schema(edges[0]) if edges else ANY
|
|
637
|
+
|
|
638
|
+
def ancestors(self, ident):
|
|
639
|
+
"""Every node that has finished before ``ident`` starts, in any run where both run."""
|
|
640
|
+
out = set()
|
|
641
|
+
for level in self.scopes(ident):
|
|
642
|
+
here = self.lift(ident, level)
|
|
643
|
+
edges = self.level_edges(level)
|
|
644
|
+
before = {a: set() for a, _b in edges}
|
|
645
|
+
for a, b in edges:
|
|
646
|
+
before.setdefault(b, set()).add(a)
|
|
647
|
+
pending = list(before.get(here, ()))
|
|
648
|
+
while pending:
|
|
649
|
+
at = pending.pop()
|
|
650
|
+
if at not in out:
|
|
651
|
+
out.add(at)
|
|
652
|
+
pending.extend(before.get(at, ()))
|
|
653
|
+
return out
|
|
654
|
+
|
|
655
|
+
def env_for(self, ident):
|
|
656
|
+
loops = self.scopes(ident)[:-1]
|
|
657
|
+
env = {"input": self.input_schema(ident), "inputs": self.values_schema, "vars": self.vars_schema,
|
|
658
|
+
"run": RUN_SCHEMA, "node": {a: self.schema_of(a) for a in self.ancestors(ident)}}
|
|
659
|
+
if loops:
|
|
660
|
+
ports = TYPES["loop"].outputs(self.nodes[loops[0]], Shapes(self, self.nodes[loops[0]]))
|
|
661
|
+
env["item"], env["index"] = ports.get("item", ANY), INTEGER
|
|
662
|
+
return env
|
|
663
|
+
|
|
664
|
+
def env_for_edge(self, edge, schema):
|
|
665
|
+
env = self.env_for(edge["target"]) if edge["target"] in self.nodes else {}
|
|
666
|
+
return {**env, "input": schema}
|
|
667
|
+
|
|
668
|
+
def trusted(self, ident, roots):
|
|
669
|
+
loops = self.scopes(ident)[:-1]
|
|
670
|
+
allowed = set(TRUSTED_ROOTS)
|
|
671
|
+
if loops and self.nodes[loops[0]]["config"].get("over") in TRUSTED_ITEMS:
|
|
672
|
+
allowed.add("item")
|
|
673
|
+
return roots <= allowed
|
|
674
|
+
|
|
675
|
+
def check_expressions(self):
|
|
676
|
+
edges = {edge["id"]: edge for edge in self.edges}
|
|
677
|
+
for owner, path, parsed, instructs, wants in self.expressions:
|
|
678
|
+
if isinstance(owner, tuple):
|
|
679
|
+
edge = edges.get(owner[0])
|
|
680
|
+
if edge is None:
|
|
681
|
+
continue
|
|
682
|
+
where = {"edge": owner[0], "field": "map"}
|
|
683
|
+
env = self.env_for_edge(edge, self.schema_of(edge["source"], edge["sourcePort"]))
|
|
684
|
+
else:
|
|
685
|
+
where = {"node": owner, "field": ".".join(str(p) for p in path if p != "whole")}
|
|
686
|
+
env = self.env_for(owner)
|
|
687
|
+
if instructs and not self.trusted(owner, parsed.roots):
|
|
688
|
+
self.problem("Screen text can't become a step's instructions. Pass it as the step's input "
|
|
689
|
+
"instead.", **where)
|
|
690
|
+
continue
|
|
691
|
+
try:
|
|
692
|
+
schema = infer(parsed, env)
|
|
693
|
+
except ExprError as error:
|
|
694
|
+
self.problem(error, **where)
|
|
695
|
+
continue
|
|
696
|
+
kind = schema_kind(schema)
|
|
697
|
+
if wants and kind is not None and kind != wants:
|
|
698
|
+
self.problem(f"This gives {_kind_name(kind)}, and it needs {_kind_name(wants)}", **where)
|
|
699
|
+
if not isinstance(owner, tuple) and path and path[-1] == "whole":
|
|
700
|
+
self.field_types.setdefault(owner, {})[path[:-1]] = schema
|
|
701
|
+
for edge in self.edges:
|
|
702
|
+
if edge["targetPort"] == "items":
|
|
703
|
+
kind = schema_kind(self.edge_schema(edge))
|
|
704
|
+
if kind is not None and kind != "list":
|
|
705
|
+
self.problem(f"A loop's items is a list, and this edge carries {_kind_name(kind)}. "
|
|
706
|
+
"Reshape it with the edge's map.", edge=edge["id"])
|
|
707
|
+
|
|
708
|
+
def check_pins(self):
|
|
709
|
+
total = 0
|
|
710
|
+
for node in self.nodes.values():
|
|
711
|
+
pin = node.get("pin")
|
|
712
|
+
if pin is None:
|
|
713
|
+
continue
|
|
714
|
+
if not isinstance(pin, dict) or not pin:
|
|
715
|
+
self.problem("A pin is test data by output port, like {\"out\": ...}", node=node["id"], field="pin")
|
|
716
|
+
continue
|
|
717
|
+
try:
|
|
718
|
+
size = _json_size(pin)
|
|
719
|
+
except (TypeError, ValueError):
|
|
720
|
+
self.problem("A pin is plain JSON", node=node["id"], field="pin")
|
|
721
|
+
continue
|
|
722
|
+
total += size
|
|
723
|
+
if size > MAX_PIN_BYTES:
|
|
724
|
+
self.problem(f"A node's pin is at most {MAX_PIN_BYTES // 1000} KB", node=node["id"], field="pin")
|
|
725
|
+
for port, value in pin.items():
|
|
726
|
+
if port not in self.outputs(node["id"]) or port == "error":
|
|
727
|
+
self.problem(f"{self.name(node['id'])} has no output called {port}", node=node["id"], field="pin")
|
|
728
|
+
continue
|
|
729
|
+
kind = schema_kind(self.schema_of(node["id"], port))
|
|
730
|
+
if kind is not None and value is not None and expr.kind_of(value) != kind:
|
|
731
|
+
self.problem(f"The pin for {port} should be {_kind_name(kind)}", node=node["id"], field="pin")
|
|
732
|
+
if total > MAX_PINS_BYTES:
|
|
733
|
+
self.problem(f"A graph's pins are at most {MAX_PINS_BYTES // 1_000_000} MB in all", field="pins")
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _kind_name(kind):
|
|
737
|
+
return {"null": "null", "boolean": "true or false", "number": "a number", "string": "text", "list": "a list",
|
|
738
|
+
"object": "an object"}.get(kind, "something")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _schema_of_value(value):
|
|
742
|
+
kind = expr.kind_of(value)
|
|
743
|
+
return {"type": {"list": "array"}.get(kind, kind)}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _strings(value, path):
|
|
747
|
+
"""(path, text) for every text inside a config value."""
|
|
748
|
+
if isinstance(value, str):
|
|
749
|
+
yield path, value
|
|
750
|
+
elif isinstance(value, dict):
|
|
751
|
+
for key, item in value.items():
|
|
752
|
+
yield from _strings(item, path + (key,))
|
|
753
|
+
elif isinstance(value, list):
|
|
754
|
+
for index, item in enumerate(value):
|
|
755
|
+
yield from _strings(item, path + (index,))
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _topological(members, edges):
|
|
759
|
+
"""``members`` in an order where every edge's source comes first (ties keep their saved order), or None."""
|
|
760
|
+
position = {m: i for i, m in enumerate(members)}
|
|
761
|
+
needs = {m: 0 for m in members}
|
|
762
|
+
after = {m: [] for m in members}
|
|
763
|
+
for a, b in edges:
|
|
764
|
+
if a in needs and b in needs:
|
|
765
|
+
needs[b] += 1
|
|
766
|
+
after[a].append(b)
|
|
767
|
+
ready = sorted((m for m in members if needs[m] == 0), key=position.get)
|
|
768
|
+
out = []
|
|
769
|
+
while ready:
|
|
770
|
+
at = ready.pop(0)
|
|
771
|
+
out.append(at)
|
|
772
|
+
for b in after[at]:
|
|
773
|
+
needs[b] -= 1
|
|
774
|
+
if needs[b] == 0:
|
|
775
|
+
ready.append(b)
|
|
776
|
+
ready.sort(key=position.get)
|
|
777
|
+
return out if len(out) == len(members) else None
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _cycle(members, edges):
|
|
781
|
+
after = {m: [b for a, b in edges if a == m] for m in members}
|
|
782
|
+
state = {}
|
|
783
|
+
|
|
784
|
+
def visit(at, trail):
|
|
785
|
+
state[at] = 1
|
|
786
|
+
for b in after.get(at, ()):
|
|
787
|
+
if state.get(b) == 1:
|
|
788
|
+
return trail[trail.index(b):] + [b] if b in trail else [at, b]
|
|
789
|
+
if b not in state:
|
|
790
|
+
found = visit(b, trail + [b])
|
|
791
|
+
if found:
|
|
792
|
+
return found
|
|
793
|
+
state[at] = 2
|
|
794
|
+
return None
|
|
795
|
+
|
|
796
|
+
for m in members:
|
|
797
|
+
if m not in state:
|
|
798
|
+
found = visit(m, [m])
|
|
799
|
+
if found:
|
|
800
|
+
return found[:-1]
|
|
801
|
+
return members[:1]
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def coerce(value, kind, name="That input"):
|
|
805
|
+
"""A run's value for an input of ``kind``: text read as a number or true or false where the input says so."""
|
|
806
|
+
if kind == "string":
|
|
807
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
808
|
+
return format(value, "g")
|
|
809
|
+
if not isinstance(value, str):
|
|
810
|
+
raise ValueError(f"{name} is text")
|
|
811
|
+
return value
|
|
812
|
+
if kind == "number":
|
|
813
|
+
if isinstance(value, bool):
|
|
814
|
+
raise ValueError(f"{name} is a number")
|
|
815
|
+
if isinstance(value, (int, float)):
|
|
816
|
+
return value
|
|
817
|
+
try:
|
|
818
|
+
return expr.evaluate("number($input)", {"input": value})
|
|
819
|
+
except ExprError:
|
|
820
|
+
raise ValueError(f"{name} is a number, not {str(value)[:40]!r}") from None
|
|
821
|
+
if kind == "boolean":
|
|
822
|
+
try:
|
|
823
|
+
return expr.evaluate("bool($input)", {"input": value})
|
|
824
|
+
except ExprError:
|
|
825
|
+
raise ValueError(f"{name} is true or false") from None
|
|
826
|
+
if kind == "list":
|
|
827
|
+
if not isinstance(value, list):
|
|
828
|
+
raise ValueError(f"{name} is a list")
|
|
829
|
+
return value
|
|
830
|
+
if not isinstance(value, dict):
|
|
831
|
+
raise ValueError(f"{name} is an object")
|
|
832
|
+
return value
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def validate(graph, context=None):
|
|
836
|
+
"""The graph as saved, or GraphError listing every problem."""
|
|
837
|
+
return _Validator(graph, context or Context()).run()
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def check_graph(graph):
|
|
841
|
+
"""Whether ``graph`` (as saved) is a graph of this version: the light check readers make before trusting it."""
|
|
842
|
+
return isinstance(graph, dict) and graph.get("version") == VERSION and isinstance(graph.get("nodes"), list)
|