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,582 @@
|
|
|
1
|
+
"""A stdlib-only MCP client: stdio (a command Mobster starts) and streamable HTTP (a URL), as mcp_server/protocol.py is
|
|
2
|
+
the server side."""
|
|
3
|
+
|
|
4
|
+
import http.client
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import queue
|
|
8
|
+
import socket
|
|
9
|
+
import ssl
|
|
10
|
+
import subprocess
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from urllib.parse import urlsplit
|
|
14
|
+
|
|
15
|
+
from .. import __version__
|
|
16
|
+
|
|
17
|
+
PROTOCOL_VERSION = "2025-06-18"
|
|
18
|
+
LIST_TIMEOUT = 20.0
|
|
19
|
+
CALL_TIMEOUT = 60.0
|
|
20
|
+
PING_TIMEOUT = 5.0
|
|
21
|
+
CONNECT_TIMEOUT = 30.0 # starting a command (npx may download its package the first time) and initialize
|
|
22
|
+
MAX_TOOLS = 500 # tools/list stops after this many
|
|
23
|
+
MAX_PAGES = 10
|
|
24
|
+
MAX_MESSAGE_BYTES = 8 * 1024 * 1024
|
|
25
|
+
STDERR_KEEP = 2000 # the last characters a stdio server wrote to stderr, for "it didn't start" messages
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConnectorError(Exception):
|
|
29
|
+
"""A failed MCP request. ``message`` is safe to show and journal: never a token, an env value or a URL's query."""
|
|
30
|
+
|
|
31
|
+
sent = None # False: nothing happened; True: the server answered; None: unknown
|
|
32
|
+
|
|
33
|
+
def __init__(self, message):
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.message = message
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NotSent(ConnectorError):
|
|
39
|
+
"""The request never reached the server (the command didn't start, the connection was refused): nothing happened."""
|
|
40
|
+
|
|
41
|
+
sent = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuthNeeded(NotSent):
|
|
45
|
+
"""The server answered 401 or 403 before it did anything: nothing happened; sign in again."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SlowToAnswer(NotSent):
|
|
49
|
+
"""A server that was started and has not answered the handshake yet: a first `npx` can take a minute to download."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ServerError(ConnectorError):
|
|
53
|
+
sent = True
|
|
54
|
+
|
|
55
|
+
def __init__(self, message, code=None):
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.code = code
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OutcomeUnknown(ConnectorError):
|
|
61
|
+
"""The request went out and no answer came in time: it may have happened, and the caller must not send it again."""
|
|
62
|
+
|
|
63
|
+
sent = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _jsonrpc(method, params, ident=None):
|
|
67
|
+
message = {"jsonrpc": "2.0", "method": method}
|
|
68
|
+
if params is not None:
|
|
69
|
+
message["params"] = params
|
|
70
|
+
if ident is not None:
|
|
71
|
+
message["id"] = ident
|
|
72
|
+
return message
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _error_of(message, name):
|
|
76
|
+
error = message.get("error")
|
|
77
|
+
if not isinstance(error, dict):
|
|
78
|
+
return None
|
|
79
|
+
text = " ".join(str(error.get("message") or "an error").split())[:300]
|
|
80
|
+
return ServerError(f"{name} answered with an error: {text}", error.get("code"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Session:
|
|
84
|
+
"""The MCP conversation over one transport."""
|
|
85
|
+
|
|
86
|
+
name = "The server"
|
|
87
|
+
|
|
88
|
+
def __init__(self):
|
|
89
|
+
self._ids = 0
|
|
90
|
+
self._lock = threading.Lock()
|
|
91
|
+
self.server_info = {}
|
|
92
|
+
self.instructions = ""
|
|
93
|
+
self.ready = False
|
|
94
|
+
# The server said its tools changed (notifications/tools/list_changed): the service lists them again before
|
|
95
|
+
# the next call.
|
|
96
|
+
self.tools_changed = False
|
|
97
|
+
|
|
98
|
+
def _next_id(self):
|
|
99
|
+
with self._lock:
|
|
100
|
+
self._ids += 1
|
|
101
|
+
return self._ids
|
|
102
|
+
|
|
103
|
+
def initialize(self, timeout=CONNECT_TIMEOUT):
|
|
104
|
+
deadline = time.monotonic() + timeout
|
|
105
|
+
answer = self._ask(_jsonrpc("initialize", {
|
|
106
|
+
"protocolVersion": PROTOCOL_VERSION, "capabilities": {},
|
|
107
|
+
"clientInfo": {"name": "Mobster", "version": __version__}}, self._next_id()), deadline, phase="connect")
|
|
108
|
+
result = answer.get("result")
|
|
109
|
+
if not isinstance(result, dict):
|
|
110
|
+
raise NotSent(f"{self.name} didn't answer like an MCP server.")
|
|
111
|
+
self.server_info = result.get("serverInfo") if isinstance(result.get("serverInfo"), dict) else {}
|
|
112
|
+
self.instructions = str(result.get("instructions") or "")[:2000]
|
|
113
|
+
self.protocol = str(result.get("protocolVersion") or PROTOCOL_VERSION)
|
|
114
|
+
self._send(_jsonrpc("notifications/initialized", None), deadline)
|
|
115
|
+
self.ready = True
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
def list_tools(self, timeout=LIST_TIMEOUT):
|
|
119
|
+
"""Every tool the server lists, as {"name", "description", "inputSchema", "annotations"} dicts."""
|
|
120
|
+
deadline = time.monotonic() + timeout
|
|
121
|
+
tools, cursor = [], None
|
|
122
|
+
for _ in range(MAX_PAGES):
|
|
123
|
+
answer = self._ask(_jsonrpc("tools/list", {"cursor": cursor} if cursor else {}, self._next_id()),
|
|
124
|
+
deadline, phase="list")
|
|
125
|
+
result = answer.get("result") or {}
|
|
126
|
+
for tool in result.get("tools") or ():
|
|
127
|
+
if isinstance(tool, dict) and isinstance(tool.get("name"), str) and tool["name"]:
|
|
128
|
+
tools.append(tool)
|
|
129
|
+
cursor = result.get("nextCursor")
|
|
130
|
+
if not cursor or len(tools) >= MAX_TOOLS:
|
|
131
|
+
break
|
|
132
|
+
return tools[:MAX_TOOLS]
|
|
133
|
+
|
|
134
|
+
def ping(self, timeout=PING_TIMEOUT):
|
|
135
|
+
"""MCP ``ping``: the cheapest proof the session still answers."""
|
|
136
|
+
if not self.ready:
|
|
137
|
+
raise NotSent(f"{self.name} isn't connected.")
|
|
138
|
+
deadline = time.monotonic() + timeout
|
|
139
|
+
try:
|
|
140
|
+
self._ask(_jsonrpc("ping", None, self._next_id()), deadline, phase="list")
|
|
141
|
+
except ServerError:
|
|
142
|
+
pass # a server without ping still answered
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
def _notified(self, message):
|
|
146
|
+
if message.get("method") == "notifications/tools/list_changed":
|
|
147
|
+
self.tools_changed = True
|
|
148
|
+
|
|
149
|
+
def call_tool(self, name, arguments, timeout=CALL_TIMEOUT):
|
|
150
|
+
"""The tool's result ({"content", "structuredContent"?, "isError"?})."""
|
|
151
|
+
if not self.ready:
|
|
152
|
+
raise NotSent(f"{self.name} isn't connected.")
|
|
153
|
+
deadline = time.monotonic() + timeout
|
|
154
|
+
ident = self._next_id()
|
|
155
|
+
try:
|
|
156
|
+
answer = self._ask(_jsonrpc("tools/call", {"name": name, "arguments": arguments}, ident), deadline,
|
|
157
|
+
phase="call")
|
|
158
|
+
except OutcomeUnknown:
|
|
159
|
+
self._cancel(ident)
|
|
160
|
+
raise
|
|
161
|
+
result = answer.get("result")
|
|
162
|
+
if not isinstance(result, dict):
|
|
163
|
+
raise ServerError(f"{self.name} answered without a result.")
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def _cancel(self, ident):
|
|
167
|
+
try:
|
|
168
|
+
self._send(_jsonrpc("notifications/cancelled", {"requestId": ident, "reason": "timed out"}),
|
|
169
|
+
time.monotonic() + 2)
|
|
170
|
+
except ConnectorError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def _send(self, message, deadline):
|
|
174
|
+
raise NotImplementedError
|
|
175
|
+
|
|
176
|
+
def _ask(self, message, deadline, phase):
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
def close(self):
|
|
180
|
+
self.ready = False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class StdioSession(Session):
|
|
184
|
+
"""A command Mobster starts and talks to over its stdin and stdout, newline-delimited JSON-RPC."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, argv, env=None, cwd=None, name="The server"):
|
|
187
|
+
super().__init__()
|
|
188
|
+
self.argv, self.env, self.cwd, self.name = list(argv), env, cwd, name
|
|
189
|
+
self.process = None
|
|
190
|
+
self._answers = {}
|
|
191
|
+
self._waiters = {}
|
|
192
|
+
self._stderr = ""
|
|
193
|
+
self._closed = threading.Event()
|
|
194
|
+
self._write_lock = threading.Lock()
|
|
195
|
+
|
|
196
|
+
def start(self):
|
|
197
|
+
try:
|
|
198
|
+
self.process = subprocess.Popen(
|
|
199
|
+
self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env,
|
|
200
|
+
cwd=self.cwd, start_new_session=True)
|
|
201
|
+
except FileNotFoundError:
|
|
202
|
+
raise NotSent(f"Mobster couldn't find {os.path.basename(self.argv[0])}. Install it, or check the "
|
|
203
|
+
"command.") from None
|
|
204
|
+
except (OSError, ValueError) as error:
|
|
205
|
+
raise NotSent(f"Mobster couldn't start {os.path.basename(self.argv[0])} "
|
|
206
|
+
f"({type(error).__name__}).") from None
|
|
207
|
+
threading.Thread(target=self._read, name="mobster-connector-out", daemon=True).start()
|
|
208
|
+
threading.Thread(target=self._drain, name="mobster-connector-err", daemon=True).start()
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def _read(self):
|
|
212
|
+
stream = self.process.stdout
|
|
213
|
+
try:
|
|
214
|
+
for line in stream:
|
|
215
|
+
if len(line) > MAX_MESSAGE_BYTES:
|
|
216
|
+
continue
|
|
217
|
+
try:
|
|
218
|
+
message = json.loads(line)
|
|
219
|
+
except ValueError:
|
|
220
|
+
continue # a server's stray output line is skipped, as protocol.py skips a client's
|
|
221
|
+
if not isinstance(message, dict):
|
|
222
|
+
continue
|
|
223
|
+
ident = message.get("id")
|
|
224
|
+
if "method" in message:
|
|
225
|
+
if ident is None:
|
|
226
|
+
self._notified(message)
|
|
227
|
+
else:
|
|
228
|
+
# A request from the server (sampling, roots, elicitation): Mobster offers none of them.
|
|
229
|
+
self._write(_jsonrpc_error(ident, -32601, "Mobster doesn't offer this"))
|
|
230
|
+
continue
|
|
231
|
+
with self._lock:
|
|
232
|
+
waiter = self._waiters.pop(ident, None)
|
|
233
|
+
if waiter is not None:
|
|
234
|
+
waiter.put(message)
|
|
235
|
+
except (OSError, ValueError):
|
|
236
|
+
pass
|
|
237
|
+
finally:
|
|
238
|
+
self._closed.set()
|
|
239
|
+
with self._lock:
|
|
240
|
+
waiters, self._waiters = list(self._waiters.values()), {}
|
|
241
|
+
for waiter in waiters:
|
|
242
|
+
waiter.put(None)
|
|
243
|
+
|
|
244
|
+
def _drain(self):
|
|
245
|
+
try:
|
|
246
|
+
for line in self.process.stderr:
|
|
247
|
+
self._stderr = (self._stderr + line.decode(errors="replace"))[-STDERR_KEEP:]
|
|
248
|
+
except (OSError, ValueError):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
def stderr_tail(self):
|
|
252
|
+
from ..secret_filter import redact
|
|
253
|
+
return redact(" ".join(self._stderr.split())[-300:])
|
|
254
|
+
|
|
255
|
+
def _write(self, message):
|
|
256
|
+
data = (json.dumps(message, separators=(",", ":")) + "\n").encode()
|
|
257
|
+
with self._write_lock:
|
|
258
|
+
self.process.stdin.write(data)
|
|
259
|
+
self.process.stdin.flush()
|
|
260
|
+
|
|
261
|
+
def _send(self, message, deadline):
|
|
262
|
+
if self.process is None or self._closed.is_set():
|
|
263
|
+
raise NotSent(f"{self.name} isn't running.")
|
|
264
|
+
try:
|
|
265
|
+
self._write(message)
|
|
266
|
+
except (OSError, ValueError):
|
|
267
|
+
raise NotSent(f"{self.name} stopped.") from None
|
|
268
|
+
|
|
269
|
+
def _ask(self, message, deadline, phase):
|
|
270
|
+
if self.process is None or self._closed.is_set():
|
|
271
|
+
raise NotSent(self._stopped_message())
|
|
272
|
+
waiter = queue.Queue(maxsize=1)
|
|
273
|
+
with self._lock:
|
|
274
|
+
self._waiters[message["id"]] = waiter
|
|
275
|
+
try:
|
|
276
|
+
self._write(message)
|
|
277
|
+
except (OSError, ValueError):
|
|
278
|
+
with self._lock:
|
|
279
|
+
self._waiters.pop(message["id"], None)
|
|
280
|
+
raise NotSent(self._stopped_message()) from None
|
|
281
|
+
try:
|
|
282
|
+
answer = waiter.get(timeout=max(0.0, deadline - time.monotonic()))
|
|
283
|
+
except queue.Empty:
|
|
284
|
+
with self._lock:
|
|
285
|
+
self._waiters.pop(message["id"], None)
|
|
286
|
+
if phase == "call":
|
|
287
|
+
raise OutcomeUnknown(f"{self.name} didn't answer in time.") from None
|
|
288
|
+
raise SlowToAnswer(f"{self.name} didn't answer in time.") from None
|
|
289
|
+
if answer is None:
|
|
290
|
+
if phase == "call":
|
|
291
|
+
raise OutcomeUnknown(f"{self.name} stopped before it answered.")
|
|
292
|
+
raise NotSent(self._stopped_message())
|
|
293
|
+
error = _error_of(answer, self.name)
|
|
294
|
+
if error is not None:
|
|
295
|
+
if phase == "connect":
|
|
296
|
+
raise NotSent(error.message)
|
|
297
|
+
raise error
|
|
298
|
+
return answer
|
|
299
|
+
|
|
300
|
+
def _stopped_message(self):
|
|
301
|
+
tail = self.stderr_tail()
|
|
302
|
+
return f"{self.name} stopped." + (f" It said: {tail}" if tail else "")
|
|
303
|
+
|
|
304
|
+
def close(self):
|
|
305
|
+
super().close()
|
|
306
|
+
process, self.process = self.process, None
|
|
307
|
+
if process is None:
|
|
308
|
+
return
|
|
309
|
+
try:
|
|
310
|
+
process.stdin.close()
|
|
311
|
+
except OSError:
|
|
312
|
+
pass
|
|
313
|
+
try:
|
|
314
|
+
process.wait(timeout=1.0)
|
|
315
|
+
except subprocess.TimeoutExpired:
|
|
316
|
+
try:
|
|
317
|
+
os.killpg(process.pid, 15)
|
|
318
|
+
process.wait(timeout=1.0)
|
|
319
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
320
|
+
try:
|
|
321
|
+
os.killpg(process.pid, 9)
|
|
322
|
+
process.wait(timeout=1.0)
|
|
323
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
324
|
+
pass
|
|
325
|
+
for stream in (process.stdout, process.stderr):
|
|
326
|
+
try:
|
|
327
|
+
stream.close()
|
|
328
|
+
except OSError:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _jsonrpc_error(ident, code, message):
|
|
333
|
+
return {"jsonrpc": "2.0", "id": ident, "error": {"code": code, "message": message}}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def validate_url(url):
|
|
337
|
+
"""``url`` when it is an http(s) URL without credentials; plain http only on this Mac."""
|
|
338
|
+
if not isinstance(url, str):
|
|
339
|
+
raise ValueError("A server's URL is text")
|
|
340
|
+
url = url.strip()
|
|
341
|
+
parts = urlsplit(url)
|
|
342
|
+
if parts.scheme not in ("https", "http") or not parts.hostname:
|
|
343
|
+
raise ValueError("A server's URL starts with https://")
|
|
344
|
+
if parts.username or parts.password:
|
|
345
|
+
raise ValueError("Put the sign-in in the token field, not in the URL")
|
|
346
|
+
if parts.scheme == "http" and parts.hostname not in ("localhost", "127.0.0.1", "::1"):
|
|
347
|
+
raise ValueError("A server on the internet needs an https:// URL")
|
|
348
|
+
if len(url) > 2000 or any(c in url for c in "\r\n\t "):
|
|
349
|
+
raise ValueError("That URL isn't valid")
|
|
350
|
+
return url
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def unreachable(name, url, error):
|
|
354
|
+
"""Why a connection to ``url`` didn't open, in words that say what to fix (P05-3: only "couldn't reach")."""
|
|
355
|
+
parts = urlsplit(url)
|
|
356
|
+
where = f"{parts.hostname}:{parts.port}" if parts.port else parts.hostname
|
|
357
|
+
if isinstance(error, socket.gaierror):
|
|
358
|
+
return f"Mobster couldn't find {parts.hostname}. Check the address."
|
|
359
|
+
if isinstance(error, ConnectionRefusedError):
|
|
360
|
+
return f"Nothing answered at {where}. Check the address, and that the server is running."
|
|
361
|
+
if isinstance(error, ssl.SSLCertVerificationError):
|
|
362
|
+
return f"{parts.hostname}'s certificate isn't one this Mac trusts, so Mobster didn't connect."
|
|
363
|
+
if isinstance(error, TimeoutError):
|
|
364
|
+
return f"{where} didn't answer. Check the address, and that the server is running."
|
|
365
|
+
return f"Mobster couldn't reach {name}."
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def hint(url):
|
|
369
|
+
"""The scheme, host and path of ``url`` (no query): what the API and the UI show."""
|
|
370
|
+
parts = urlsplit(url)
|
|
371
|
+
host = parts.hostname or ""
|
|
372
|
+
port = f":{parts.port}" if parts.port else ""
|
|
373
|
+
return f"{parts.scheme}://{host}{port}{parts.path or ''}"[:200]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class HttpSession(Session):
|
|
377
|
+
"""Streamable HTTP: each message is a POST to the URL; an answer is JSON or an SSE stream."""
|
|
378
|
+
|
|
379
|
+
def __init__(self, url, token=None, name="The server", headers=None):
|
|
380
|
+
super().__init__()
|
|
381
|
+
self.url, self.token, self.name = validate_url(url), token, name
|
|
382
|
+
self.extra_headers = dict(headers or {})
|
|
383
|
+
self.session_id = None
|
|
384
|
+
self.protocol = None
|
|
385
|
+
self.resource_metadata = None # from a 401's WWW-Authenticate: where its OAuth metadata is
|
|
386
|
+
|
|
387
|
+
def start(self):
|
|
388
|
+
return self
|
|
389
|
+
|
|
390
|
+
def _headers(self):
|
|
391
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
|
|
392
|
+
"User-Agent": f"Mobster/{__version__}", **self.extra_headers}
|
|
393
|
+
if self.token:
|
|
394
|
+
headers["Authorization"] = "Bearer " + self.token
|
|
395
|
+
if self.session_id:
|
|
396
|
+
headers["Mcp-Session-Id"] = self.session_id
|
|
397
|
+
if self.protocol:
|
|
398
|
+
headers["MCP-Protocol-Version"] = self.protocol
|
|
399
|
+
return headers
|
|
400
|
+
|
|
401
|
+
def _connection(self, deadline):
|
|
402
|
+
parts = urlsplit(self.url)
|
|
403
|
+
cls = http.client.HTTPSConnection if parts.scheme == "https" else http.client.HTTPConnection
|
|
404
|
+
left = deadline - time.monotonic()
|
|
405
|
+
if left <= 0:
|
|
406
|
+
raise NotSent(f"{self.name} didn't answer in time.")
|
|
407
|
+
return cls(parts.hostname, parts.port, timeout=left), (parts.path or "/") + (
|
|
408
|
+
f"?{parts.query}" if parts.query else "")
|
|
409
|
+
|
|
410
|
+
def _post(self, message, deadline, phase, expect_answer):
|
|
411
|
+
connection, path = self._connection(deadline)
|
|
412
|
+
body = json.dumps(message, separators=(",", ":")).encode()
|
|
413
|
+
finished = threading.Event()
|
|
414
|
+
|
|
415
|
+
def expire():
|
|
416
|
+
if finished.is_set():
|
|
417
|
+
return
|
|
418
|
+
sock = connection.sock
|
|
419
|
+
if sock is not None:
|
|
420
|
+
try:
|
|
421
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
422
|
+
except OSError:
|
|
423
|
+
pass
|
|
424
|
+
connection.close()
|
|
425
|
+
|
|
426
|
+
timer = threading.Timer(max(0.0, deadline - time.monotonic()), expire)
|
|
427
|
+
timer.daemon = True
|
|
428
|
+
timer.start()
|
|
429
|
+
sent = False
|
|
430
|
+
try:
|
|
431
|
+
try:
|
|
432
|
+
connection.connect()
|
|
433
|
+
except (OSError, http.client.HTTPException) as error:
|
|
434
|
+
raise NotSent(unreachable(self.name, self.url, error)) from None
|
|
435
|
+
try:
|
|
436
|
+
connection.request("POST", path, body, self._headers())
|
|
437
|
+
sent = True
|
|
438
|
+
response = connection.getresponse()
|
|
439
|
+
except (OSError, http.client.HTTPException):
|
|
440
|
+
if sent and phase == "call":
|
|
441
|
+
raise OutcomeUnknown(f"{self.name} didn't answer in time.") from None
|
|
442
|
+
raise NotSent(f"{self.name} didn't answer.") from None
|
|
443
|
+
status = response.status
|
|
444
|
+
if status in (401, 403):
|
|
445
|
+
challenge = response.getheader("WWW-Authenticate") or ""
|
|
446
|
+
self.resource_metadata = _resource_metadata(challenge)
|
|
447
|
+
response.read(64_000)
|
|
448
|
+
raise AuthNeeded(f"{self.name} needs you to sign in again." if self.token else
|
|
449
|
+
f"{self.name} needs you to sign in.")
|
|
450
|
+
if status == 404 and self.session_id and phase != "connect":
|
|
451
|
+
response.read(64_000)
|
|
452
|
+
self.ready = False
|
|
453
|
+
raise NotSent(f"{self.name} ended the session. Try again.")
|
|
454
|
+
session = response.getheader("Mcp-Session-Id")
|
|
455
|
+
if session and phase == "connect":
|
|
456
|
+
self.session_id = session.strip()[:200]
|
|
457
|
+
if not expect_answer:
|
|
458
|
+
response.read(64_000)
|
|
459
|
+
if status >= 400:
|
|
460
|
+
raise NotSent(f"{self.name} answered HTTP {status}.")
|
|
461
|
+
return None
|
|
462
|
+
if status >= 400:
|
|
463
|
+
response.read(64_000)
|
|
464
|
+
# 4xx: the server refused it before doing anything; 5xx after it was sent may be either.
|
|
465
|
+
if status >= 500 and phase == "call":
|
|
466
|
+
raise OutcomeUnknown(f"{self.name} answered HTTP {status}.")
|
|
467
|
+
raise NotSent(f"{self.name} answered HTTP {status}.")
|
|
468
|
+
kind = (response.getheader("Content-Type") or "").split(";")[0].strip().lower()
|
|
469
|
+
try:
|
|
470
|
+
if kind == "text/event-stream":
|
|
471
|
+
answer = _from_events(response, message.get("id"), self._notified)
|
|
472
|
+
else:
|
|
473
|
+
raw = response.read(MAX_MESSAGE_BYTES + 1)
|
|
474
|
+
if len(raw) > MAX_MESSAGE_BYTES:
|
|
475
|
+
raise ValueError("too large")
|
|
476
|
+
answer = json.loads(raw)
|
|
477
|
+
except (OSError, http.client.HTTPException, ValueError):
|
|
478
|
+
if phase == "call":
|
|
479
|
+
raise OutcomeUnknown(f"{self.name}'s answer was cut off.") from None
|
|
480
|
+
raise NotSent(f"{self.name}'s answer was cut off.") from None
|
|
481
|
+
if isinstance(answer, list):
|
|
482
|
+
answer = next((m for m in answer if isinstance(m, dict) and m.get("id") == message.get("id")), None)
|
|
483
|
+
if not isinstance(answer, dict):
|
|
484
|
+
if phase == "call":
|
|
485
|
+
raise OutcomeUnknown(f"{self.name} didn't answer like an MCP server.")
|
|
486
|
+
raise NotSent(f"{self.name} didn't answer like an MCP server.")
|
|
487
|
+
return answer
|
|
488
|
+
finally:
|
|
489
|
+
finished.set()
|
|
490
|
+
timer.cancel()
|
|
491
|
+
connection.close()
|
|
492
|
+
|
|
493
|
+
def _send(self, message, deadline):
|
|
494
|
+
self._post(message, deadline, "notify", expect_answer=False)
|
|
495
|
+
|
|
496
|
+
def _ask(self, message, deadline, phase):
|
|
497
|
+
answer = self._post(message, deadline, phase, expect_answer=True)
|
|
498
|
+
error = _error_of(answer, self.name)
|
|
499
|
+
if error is not None:
|
|
500
|
+
if phase == "connect":
|
|
501
|
+
raise NotSent(error.message)
|
|
502
|
+
raise error
|
|
503
|
+
return answer
|
|
504
|
+
|
|
505
|
+
def close(self):
|
|
506
|
+
if self.session_id and self.ready:
|
|
507
|
+
try:
|
|
508
|
+
connection, path = self._connection(time.monotonic() + 3)
|
|
509
|
+
connection.request("DELETE", path, headers=self._headers())
|
|
510
|
+
connection.getresponse().read(1000)
|
|
511
|
+
connection.close()
|
|
512
|
+
except (OSError, http.client.HTTPException, ConnectorError):
|
|
513
|
+
pass
|
|
514
|
+
super().close()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _resource_metadata(challenge):
|
|
518
|
+
"""The ``resource_metadata`` URL of a WWW-Authenticate: Bearer challenge, or None."""
|
|
519
|
+
for part in challenge.split(","):
|
|
520
|
+
key, _, value = part.strip().partition("=")
|
|
521
|
+
if key.strip().lower().endswith("resource_metadata"):
|
|
522
|
+
value = value.strip().strip('"')
|
|
523
|
+
if value.startswith("https://") or value.startswith("http://127.0.0.1") or \
|
|
524
|
+
value.startswith("http://localhost"):
|
|
525
|
+
return value[:2000]
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _from_events(response, ident, notified=None):
|
|
530
|
+
"""The JSON-RPC answer with ``ident`` from an SSE stream."""
|
|
531
|
+
data, size = [], 0
|
|
532
|
+
while True:
|
|
533
|
+
line = response.readline(MAX_MESSAGE_BYTES)
|
|
534
|
+
if not line:
|
|
535
|
+
raise ValueError("stream ended")
|
|
536
|
+
size += len(line)
|
|
537
|
+
if size > MAX_MESSAGE_BYTES:
|
|
538
|
+
raise ValueError("too large")
|
|
539
|
+
text = line.decode(errors="replace").rstrip("\r\n")
|
|
540
|
+
if text.startswith("data:"):
|
|
541
|
+
data.append(text[5:].lstrip(" "))
|
|
542
|
+
continue
|
|
543
|
+
if text == "" and data:
|
|
544
|
+
payload, data = "\n".join(data), []
|
|
545
|
+
try:
|
|
546
|
+
message = json.loads(payload)
|
|
547
|
+
except ValueError:
|
|
548
|
+
continue
|
|
549
|
+
if isinstance(message, dict) and message.get("id") == ident and "method" not in message:
|
|
550
|
+
return message
|
|
551
|
+
if isinstance(message, dict) and "method" in message and message.get("id") is None and notified:
|
|
552
|
+
notified(message)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# A server whose tools Mobster answers itself with the vendor's web API, because the vendor's MCP server refuses
|
|
556
|
+
# Mobster's app: Slack's takes only Marketplace and internal apps (slack_api.py).
|
|
557
|
+
SLACK = "https://mcp.slack.com/mcp"
|
|
558
|
+
# Microsoft 365 has no MCP server a Mobster app can sign in to: its tools are Microsoft Graph's (graph_api.py).
|
|
559
|
+
GRAPH = "https://graph.microsoft.com/v1.0"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def connect(spec, *, env=None, token=None, timeout=CONNECT_TIMEOUT, name="The server"):
|
|
563
|
+
"""A started, initialized session for ``spec`` ({"transport": "stdio", "command", "args"} or {"transport": "http",
|
|
564
|
+
"url"})."""
|
|
565
|
+
if spec.get("transport") != "stdio" and spec.get("url") == SLACK:
|
|
566
|
+
from . import slack_api # here, not at the top: slack_api imports this module
|
|
567
|
+
return slack_api.connect(token=token, timeout=timeout, name=name)
|
|
568
|
+
if spec.get("transport") != "stdio" and spec.get("url") == GRAPH:
|
|
569
|
+
from . import graph_api # here, not at the top: graph_api imports this module
|
|
570
|
+
return graph_api.connect(token=token, timeout=timeout, name=name)
|
|
571
|
+
if spec.get("transport") == "stdio":
|
|
572
|
+
session = StdioSession([spec["command"], *spec.get("args", ())], env=env, name=name)
|
|
573
|
+
else:
|
|
574
|
+
session = HttpSession(spec["url"], token=token, name=name)
|
|
575
|
+
session.start()
|
|
576
|
+
try:
|
|
577
|
+
session.initialize(timeout=timeout)
|
|
578
|
+
except BaseException:
|
|
579
|
+
session.close()
|
|
580
|
+
raise
|
|
581
|
+
return session
|
|
582
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Mobster Checks, Mobster's GitHub App for pull requests' checks (docs/ci/github-app.mdx): the agent's half."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .. import plan_gate, share
|
|
6
|
+
from ..api_errors import APIError
|
|
7
|
+
|
|
8
|
+
PRESET = "github"
|
|
9
|
+
INSTALL_URL = "https://github.com/apps/mobster-checks/installations/new"
|
|
10
|
+
REPO = re.compile(r"[A-Za-z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}")
|
|
11
|
+
TOKEN = re.compile(r"[A-Za-z0-9_]{20,255}")
|
|
12
|
+
EXPIRES = re.compile(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ")
|
|
13
|
+
NO_GITHUB = ("Connect GitHub in Mobster (Connectors), so Mobster Checks can tell you can push to this repository. "
|
|
14
|
+
"Then run the checks again.")
|
|
15
|
+
SIGN_IN = "Sign in to Mobster, then run the checks again."
|
|
16
|
+
CODES = ("not_installed", "no_access", "not_allowed", "plan_required", "github_sign_in", "sign_in", "unconfigured")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def account_file(runtime):
|
|
20
|
+
"""The account.json this agent's plan check reads, else the Mac app's."""
|
|
21
|
+
gate = getattr(runtime, "plan_gate", None)
|
|
22
|
+
return gate.account_file if gate is not None else plan_gate.app_account_file()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def token(service, repo, account, confirm=None):
|
|
26
|
+
"""{token, expiresAt, repo} for ``repo`` (owner/name), or APIError with the reason and what to do. ``confirm(repo)``
|
|
27
|
+
runs before Mobster's server is asked: it raises to refuse."""
|
|
28
|
+
if not isinstance(repo, str) or not REPO.fullmatch(repo) or repo.split("/")[1] in (".", ".."):
|
|
29
|
+
raise APIError("repo must be a GitHub repository as owner/name.", 400, "invalid_repo")
|
|
30
|
+
stored = plan_gate.stored_account(account)
|
|
31
|
+
if stored is None:
|
|
32
|
+
raise APIError(SIGN_IN, 401, "sign_in")
|
|
33
|
+
github = service.preset_bearer(PRESET)
|
|
34
|
+
if not github:
|
|
35
|
+
raise APIError(NO_GITHUB, 409, "no_github")
|
|
36
|
+
if confirm is not None:
|
|
37
|
+
confirm(repo)
|
|
38
|
+
try:
|
|
39
|
+
status, value = share.call("POST", "/api/github/token", stored["entitlement"]["token"],
|
|
40
|
+
{"repo": repo, "github": github})
|
|
41
|
+
except share.ShareError as error:
|
|
42
|
+
raise APIError(str(error), error.status, error.code) from None
|
|
43
|
+
finally:
|
|
44
|
+
github = None # noqa: F841 -- the sign-in's only other reference goes with this frame
|
|
45
|
+
if status == 200:
|
|
46
|
+
minted, expires, named = value.get("token"), value.get("expires_at"), value.get("repo")
|
|
47
|
+
if not (isinstance(minted, str) and TOKEN.fullmatch(minted) and isinstance(expires, str)
|
|
48
|
+
and EXPIRES.fullmatch(expires) and isinstance(named, str) and REPO.fullmatch(named)):
|
|
49
|
+
raise APIError("Mobster's server answered without a token. Run the checks again.", 502, "error")
|
|
50
|
+
return {"token": minted, "expiresAt": expires, "repo": named}
|
|
51
|
+
code = value.get("error") if value.get("error") in CODES else "error"
|
|
52
|
+
message = value.get("message") if isinstance(value.get("message"), str) else \
|
|
53
|
+
"Mobster's server couldn't get a GitHub token. Run the checks again in a minute."
|
|
54
|
+
extra = {"installUrl": INSTALL_URL} if code in ("not_installed", "not_allowed") else {}
|
|
55
|
+
raise APIError(message[:400], status if 400 <= status < 600 else 502, code, **extra)
|