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,223 @@
|
|
|
1
|
+
"""Finding the Mac's own tools, and asking the Mac short questions."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .devices import LOOPBACK, UDID_PATTERN
|
|
13
|
+
|
|
14
|
+
TOOL_DIRS = ("/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/Applications/Xcode.app/Contents/Developer/usr/bin")
|
|
15
|
+
# The iPhone connection tools: libimobiledevice's and libusbmuxd's. Mobster for Mac carries its own build of them
|
|
16
|
+
# (desktop/scripts/build-iphone-tools.sh), found before any Homebrew copy, so nobody needs Homebrew or Terminal.
|
|
17
|
+
IPHONE_TOOLS = ("idevice_id", "ideviceinfo", "ideviceinstaller", "iproxy", "afcclient", "idevicesyslog",
|
|
18
|
+
"idevicecrashreport")
|
|
19
|
+
# /usr/bin/xcodebuild and /usr/bin/git exist on every Mac: they are stubs that forward to the active
|
|
20
|
+
# developer directory (and fail, or open an install dialog, without one). They never count as Xcode or
|
|
21
|
+
# git: both come from the Xcode app itself, git also from the Command Line Tools or Homebrew.
|
|
22
|
+
XCODE_SHIMS = frozenset({"xcodebuild", "git"})
|
|
23
|
+
CLT_DIR = "/Library/Developer/CommandLineTools"
|
|
24
|
+
XCODE_APP_DIRS = ("/Applications", str(Path.home() / "Applications"))
|
|
25
|
+
GIT_DIRS = ("/opt/homebrew/bin", "/usr/local/bin", CLT_DIR + "/usr/bin")
|
|
26
|
+
# Xcode's state takes a few short xcodebuild calls: a working Xcode is re-checked rarely, one that
|
|
27
|
+
# needs fixing often enough that the fix shows up on its own.
|
|
28
|
+
XCODE_OK_TTL, XCODE_RETRY_TTL = 300.0, 10.0
|
|
29
|
+
WDA_PORT, VIDEO_PORT = 8100, 9100
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def executable(directory, name):
|
|
33
|
+
path = Path(directory) / name
|
|
34
|
+
return str(path) if path.is_file() and os.access(path, os.X_OK) else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bundled_tools_dir():
|
|
38
|
+
override = os.environ.get("MOBSTER_IPHONE_TOOLS")
|
|
39
|
+
if override:
|
|
40
|
+
return Path(override)
|
|
41
|
+
frozen = getattr(sys, "_MEIPASS", None)
|
|
42
|
+
if not frozen:
|
|
43
|
+
return None
|
|
44
|
+
for resources in (Path(frozen).parent / "Resources", Path(sys.executable).resolve().parent.parent / "Resources"):
|
|
45
|
+
if (resources / "iphone-tools" / "bin").is_dir():
|
|
46
|
+
return resources / "iphone-tools" / "bin"
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bundled_tool(name):
|
|
51
|
+
"""The app's own copy of an iPhone connection tool, or None."""
|
|
52
|
+
directory = bundled_tools_dir() if name in IPHONE_TOOLS else None
|
|
53
|
+
return executable(directory, name) if directory else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tool(name):
|
|
57
|
+
if name in XCODE_SHIMS:
|
|
58
|
+
return xcode_tool(name)
|
|
59
|
+
if path := bundled_tool(name):
|
|
60
|
+
return path
|
|
61
|
+
for directory in TOOL_DIRS:
|
|
62
|
+
if path := executable(directory, name):
|
|
63
|
+
return path
|
|
64
|
+
return shutil.which(name)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_xcode(path):
|
|
68
|
+
"""A developer directory inside an Xcode app (not the Command Line Tools)."""
|
|
69
|
+
path = Path(path)
|
|
70
|
+
return (path.name == "Developer" and path.parent.name == "Contents" and path.parent.parent.suffix == ".app"
|
|
71
|
+
and executable(path / "usr" / "bin", "xcodebuild") is not None)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def developer_dir():
|
|
75
|
+
selected = (run(["/usr/bin/xcode-select", "-p"], timeout=5) or "").strip().rstrip("/") or None
|
|
76
|
+
if selected and is_xcode(selected):
|
|
77
|
+
return selected, selected
|
|
78
|
+
for root in XCODE_APP_DIRS:
|
|
79
|
+
try:
|
|
80
|
+
apps = sorted(Path(root).glob("Xcode*.app"), key=lambda app: (app.name != "Xcode.app", app.name))
|
|
81
|
+
except OSError:
|
|
82
|
+
continue
|
|
83
|
+
for app in apps:
|
|
84
|
+
if is_xcode(app / "Contents" / "Developer"):
|
|
85
|
+
return str(app / "Contents" / "Developer"), selected
|
|
86
|
+
return None, selected
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# A shell's compiler settings that clang and xcodebuild read from the environment. Meant for building for this Mac
|
|
90
|
+
# (Homebrew, conda), they point an iPhone or simulator build at the Mac's headers and SDK: on 17 Oct a shell with
|
|
91
|
+
# CPATH=/usr/local/include failed the helper's first build ("include location '/usr/local/include' is unsafe for
|
|
92
|
+
# cross-compilation", -Werror). Xcode's builds never take them.
|
|
93
|
+
HOST_BUILD_ENV = ("CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH", "OBJCPLUS_INCLUDE_PATH",
|
|
94
|
+
"LIBRARY_PATH", "SDKROOT", "CFLAGS", "CXXFLAGS", "OBJCFLAGS", "CPPFLAGS", "LDFLAGS")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def xcode_env(developer=None, base=None):
|
|
98
|
+
env = {key: value for key, value in (os.environ if base is None else base).items() if key not in HOST_BUILD_ENV}
|
|
99
|
+
if developer:
|
|
100
|
+
env["DEVELOPER_DIR"] = developer
|
|
101
|
+
return env
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# In place of asking for `sudo xcode-select -s`: only this process's environment changes, never the Mac's setting.
|
|
105
|
+
def adopt_xcode(environ=None):
|
|
106
|
+
environ = os.environ if environ is None else environ
|
|
107
|
+
if environ.get("DEVELOPER_DIR"):
|
|
108
|
+
return environ["DEVELOPER_DIR"]
|
|
109
|
+
xcode, selected = developer_dir()
|
|
110
|
+
if xcode and selected != xcode:
|
|
111
|
+
environ["DEVELOPER_DIR"] = xcode
|
|
112
|
+
return xcode
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def xcode_tool(name, developer=...):
|
|
116
|
+
developer = developer_dir()[0] if developer is ... else developer
|
|
117
|
+
directories = (([str(Path(developer) / "usr" / "bin")] if developer else [])
|
|
118
|
+
+ (list(GIT_DIRS) if name == "git" else []))
|
|
119
|
+
return next((path for directory in directories if (path := executable(directory, name))), None)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def execute(command, env=None, timeout=20):
|
|
123
|
+
"""(exit code, stdout and stderr) of a short command; (None, "") if it could not run."""
|
|
124
|
+
try:
|
|
125
|
+
completed = subprocess.run(command, capture_output=True, text=True, timeout=timeout, env=env,
|
|
126
|
+
stdin=subprocess.DEVNULL)
|
|
127
|
+
except (OSError, subprocess.SubprocessError):
|
|
128
|
+
return None, ""
|
|
129
|
+
return completed.returncode, (completed.stdout or "") + (completed.stderr or "")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# The Mac's xcode-select choice is never a problem: Mobster runs Xcode's tools with DEVELOPER_DIR (adopt_xcode).
|
|
133
|
+
def xcode_status():
|
|
134
|
+
xcode, selected = developer_dir()
|
|
135
|
+
status = {"state": "ok", "app": str(Path(xcode).parent.parent) if xcode else None, "developer_dir": xcode,
|
|
136
|
+
"selected": selected, "version": None, "problems": [], "fixes": []}
|
|
137
|
+
problems, fixes = status["problems"], status["fixes"]
|
|
138
|
+
if not xcode:
|
|
139
|
+
problems.append("missing")
|
|
140
|
+
else:
|
|
141
|
+
xcodebuild, env = str(Path(xcode) / "usr" / "bin" / "xcodebuild"), {**os.environ, "DEVELOPER_DIR": xcode}
|
|
142
|
+
code, output = execute([xcodebuild, "-version"], env)
|
|
143
|
+
if code == 0:
|
|
144
|
+
status["version"] = (output.strip().splitlines() or [None])[0]
|
|
145
|
+
if execute([xcodebuild, "-checkFirstLaunchStatus"], env)[0] not in (0, None):
|
|
146
|
+
problems.append("first_launch")
|
|
147
|
+
fixes.append("sudo xcodebuild -runFirstLaunch")
|
|
148
|
+
else:
|
|
149
|
+
code, sdks = execute([xcodebuild, "-showsdks"], env, timeout=30)
|
|
150
|
+
if code == 0 and "iphoneos" not in sdks:
|
|
151
|
+
problems.append("no_ios")
|
|
152
|
+
fixes.append("xcodebuild -downloadPlatform iOS")
|
|
153
|
+
elif "license" in output.lower():
|
|
154
|
+
problems.append("license")
|
|
155
|
+
fixes.append("sudo xcodebuild -license accept")
|
|
156
|
+
else:
|
|
157
|
+
problems.append("broken")
|
|
158
|
+
status["state"] = problems[0] if problems else "ok"
|
|
159
|
+
return status
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def count_iphones(items, simulated=False):
|
|
163
|
+
"""iPhones in a system_profiler USB tree; a virtual one on a simulated bus doesn't count."""
|
|
164
|
+
total = 0
|
|
165
|
+
for item in items if isinstance(items, list) else ():
|
|
166
|
+
if not isinstance(item, dict):
|
|
167
|
+
continue
|
|
168
|
+
on_simulated = simulated or item.get("USBKeyHardwareType") == "Simulated"
|
|
169
|
+
name = str(item.get("_name") or "")
|
|
170
|
+
vendor = str(item.get("USBDeviceKeyVendorID") or item.get("vendor_id") or "").lower()
|
|
171
|
+
if name.startswith("iPhone") and "virtual" not in name.lower() and "0x05ac" in vendor and not on_simulated:
|
|
172
|
+
total += 1
|
|
173
|
+
total += count_iphones(item.get("_items"), on_simulated)
|
|
174
|
+
return total
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def run(command, timeout=10):
|
|
178
|
+
"""stdout of a short command, or None if it failed."""
|
|
179
|
+
try:
|
|
180
|
+
completed = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
|
181
|
+
except (OSError, subprocess.SubprocessError):
|
|
182
|
+
return None
|
|
183
|
+
return completed.stdout if completed.returncode == 0 else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# How long one USB listing answers ``usb_attached``: a pulled cable is known within this many seconds.
|
|
187
|
+
ATTACHED_TTL = 2.0
|
|
188
|
+
_attached_cache = {"at": None, "udids": None}
|
|
189
|
+
_attached_lock = threading.Lock()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def usb_attached(udid, now=None, ttl=ATTACHED_TTL):
|
|
193
|
+
if not isinstance(udid, str) or not UDID_PATTERN.fullmatch(udid):
|
|
194
|
+
return None
|
|
195
|
+
now = time.monotonic() if now is None else now
|
|
196
|
+
with _attached_lock:
|
|
197
|
+
at, udids = _attached_cache["at"], _attached_cache["udids"]
|
|
198
|
+
if at is None or now - at >= ttl:
|
|
199
|
+
listing = tool("idevice_id")
|
|
200
|
+
output = run([listing, "-l"], timeout=3) if listing else None
|
|
201
|
+
udids = None if output is None else {line.strip().upper() for line in output.splitlines() if line.strip()}
|
|
202
|
+
_attached_cache.update(at=now, udids=udids)
|
|
203
|
+
return None if udids is None else udid.upper() in udids
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@functools.lru_cache(maxsize=4)
|
|
207
|
+
def iproxy_binds_loopback(path):
|
|
208
|
+
"""Whether this iproxy takes -s; an older one cannot be kept off the LAN."""
|
|
209
|
+
try:
|
|
210
|
+
completed = subprocess.run([path, "-h"], capture_output=True, text=True, timeout=5)
|
|
211
|
+
except (OSError, subprocess.SubprocessError):
|
|
212
|
+
return False
|
|
213
|
+
return "--source" in completed.stdout + completed.stderr
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def relay_command(udid, wda_port=WDA_PORT, video_port=VIDEO_PORT):
|
|
217
|
+
iproxy = tool("iproxy")
|
|
218
|
+
if not iproxy:
|
|
219
|
+
raise LookupError("The iPhone connection tools aren't installed. Open Setup › Get your Mac ready.")
|
|
220
|
+
if not iproxy_binds_loopback(iproxy):
|
|
221
|
+
raise LookupError("This iproxy can't be limited to this Mac. Update it: "
|
|
222
|
+
"brew upgrade libimobiledevice libusbmuxd")
|
|
223
|
+
return [iproxy, "-s", LOOPBACK, "-u", udid, f"{wda_port}:{WDA_PORT}", f"{video_port}:{VIDEO_PORT}"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Direct control of the phone from the dashboard's live view; the server refuses it while a task holds the phone."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .drivers import WDA
|
|
8
|
+
from .state import validate_input_text
|
|
9
|
+
from .wda.gestures import WDA_TAP_HOLD_MS
|
|
10
|
+
|
|
11
|
+
BUTTONS = {"home": "home", "volume_up": "volumeUp", "volume_down": "volumeDown"}
|
|
12
|
+
MAX_HOLD_MS = 3000
|
|
13
|
+
SWIPE_MS = (40, 3000)
|
|
14
|
+
SIZE_TTL_SECONDS = 30
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _unit(value, name):
|
|
18
|
+
if type(value) not in (int, float) or not 0 <= value <= 1:
|
|
19
|
+
raise ValueError(f"{name} must be a number from 0 to 1")
|
|
20
|
+
return float(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_command(body):
|
|
24
|
+
"""A validated gesture, or ValueError. Unknown fields are refused, never ignored."""
|
|
25
|
+
if not isinstance(body, dict):
|
|
26
|
+
raise ValueError("A control command must be an object")
|
|
27
|
+
kind = body.get("action")
|
|
28
|
+
allowed = {"tap": {"x", "y", "holdMs"}, "swipe": {"x1", "y1", "x2", "y2", "durationMs"},
|
|
29
|
+
"type": {"text", "submit"}, "button": {"name"}}
|
|
30
|
+
if kind not in allowed:
|
|
31
|
+
raise ValueError("action must be tap, swipe, type or button")
|
|
32
|
+
extra = set(body) - allowed[kind] - {"action"}
|
|
33
|
+
if extra:
|
|
34
|
+
raise ValueError(f"Unsupported field: {sorted(extra)[0]}")
|
|
35
|
+
if kind == "tap":
|
|
36
|
+
hold = body.get("holdMs", WDA_TAP_HOLD_MS)
|
|
37
|
+
if type(hold) is not int or not 0 <= hold <= MAX_HOLD_MS:
|
|
38
|
+
raise ValueError(f"holdMs must be 0 to {MAX_HOLD_MS}")
|
|
39
|
+
return {"action": "tap", "x": _unit(body.get("x"), "x"), "y": _unit(body.get("y"), "y"), "holdMs": hold}
|
|
40
|
+
if kind == "swipe":
|
|
41
|
+
duration = body.get("durationMs", 250)
|
|
42
|
+
if type(duration) is not int or not SWIPE_MS[0] <= duration <= SWIPE_MS[1]:
|
|
43
|
+
raise ValueError(f"durationMs must be {SWIPE_MS[0]} to {SWIPE_MS[1]}")
|
|
44
|
+
return {"action": "swipe", **{k: _unit(body.get(k), k) for k in ("x1", "y1", "x2", "y2")},
|
|
45
|
+
"durationMs": duration}
|
|
46
|
+
if kind == "type":
|
|
47
|
+
submit = body.get("submit", False)
|
|
48
|
+
if type(submit) is not bool:
|
|
49
|
+
raise ValueError("submit must be true or false")
|
|
50
|
+
return {"action": "type", "text": validate_input_text(body.get("text")), "submit": submit}
|
|
51
|
+
if body.get("name") not in BUTTONS:
|
|
52
|
+
raise ValueError("name must be home, volume_up or volume_down")
|
|
53
|
+
return {"action": "button", "name": body["name"]}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ManualControl:
|
|
57
|
+
def __init__(self, wda_url, session_provider):
|
|
58
|
+
self.wda_url, self.session_provider = wda_url, session_provider
|
|
59
|
+
self.lock = threading.Lock()
|
|
60
|
+
self.driver = None
|
|
61
|
+
self.session = None
|
|
62
|
+
self.size = None # (width, height, read_at)
|
|
63
|
+
|
|
64
|
+
def _driver(self):
|
|
65
|
+
session = self.session_provider()
|
|
66
|
+
if not session:
|
|
67
|
+
raise ConnectionError("Your iPhone is not connected")
|
|
68
|
+
if self.driver is None or session != self.session:
|
|
69
|
+
self.close()
|
|
70
|
+
self.driver, self.session, self.size = WDA(self.wda_url, session), session, None
|
|
71
|
+
return self.driver
|
|
72
|
+
|
|
73
|
+
def _points(self, driver):
|
|
74
|
+
"""The screen in points. /wda/screen measures through SpringBoard; /window/size
|
|
75
|
+
goes through the "active" app, which can be a transient system overlay (measured
|
|
76
|
+
on iOS 26: an 8-10 s stall, then a stale-element error), so it is only a fallback."""
|
|
77
|
+
if self.size is None or time.monotonic() - self.size[2] > SIZE_TTL_SECONDS:
|
|
78
|
+
size = None
|
|
79
|
+
try:
|
|
80
|
+
screen = WDA.response_value(driver.http.request("GET", "/wda/screen", None, 3))
|
|
81
|
+
size = screen.get("screenSize") if isinstance(screen, dict) else None
|
|
82
|
+
except Exception:
|
|
83
|
+
size = None
|
|
84
|
+
if not self._valid(size):
|
|
85
|
+
size = driver.call("GET", "/window/size", timeout=3)
|
|
86
|
+
if not self._valid(size):
|
|
87
|
+
raise ConnectionError("Your iPhone did not report its screen size")
|
|
88
|
+
self.size = (float(size["width"]), float(size["height"]), time.monotonic())
|
|
89
|
+
return self.size[:2]
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _valid(size):
|
|
93
|
+
return isinstance(size, dict) and all(
|
|
94
|
+
type(size.get(k)) in (int, float) and math.isfinite(size[k]) and size[k] > 0 for k in ("width", "height"))
|
|
95
|
+
|
|
96
|
+
def perform(self, body, teach=None):
|
|
97
|
+
"""Send one gesture; with ``teach``, the screen is read first and nothing is sent when it can't be."""
|
|
98
|
+
command = parse_command(body)
|
|
99
|
+
with self.lock:
|
|
100
|
+
driver = self._driver()
|
|
101
|
+
before = teach.read(driver) if teach is not None else None
|
|
102
|
+
try:
|
|
103
|
+
self._dispatch(driver, command)
|
|
104
|
+
except Exception:
|
|
105
|
+
# A dropped session is re-resolved on the next gesture.
|
|
106
|
+
self.close()
|
|
107
|
+
raise
|
|
108
|
+
if teach is not None:
|
|
109
|
+
teach.record(command, before)
|
|
110
|
+
return command["action"]
|
|
111
|
+
|
|
112
|
+
def observe(self, timeout=8):
|
|
113
|
+
"""The screen now (a teach session's end screen)."""
|
|
114
|
+
with self.lock:
|
|
115
|
+
return self._driver().observe(timeout=timeout)
|
|
116
|
+
|
|
117
|
+
def _dispatch(self, driver, command):
|
|
118
|
+
kind = command["action"]
|
|
119
|
+
if kind == "button":
|
|
120
|
+
driver.call("POST", "/wda/pressButton", {"name": BUTTONS[command["name"]]}, timeout=5)
|
|
121
|
+
return
|
|
122
|
+
if kind == "type":
|
|
123
|
+
keys = [command["text"], "\n"] if command["submit"] else [command["text"]]
|
|
124
|
+
driver.call("POST", "/wda/keys", {"value": keys}, timeout=10)
|
|
125
|
+
return
|
|
126
|
+
width, height = self._points(driver)
|
|
127
|
+
if kind == "tap":
|
|
128
|
+
x, y = round(command["x"] * width), round(command["y"] * height)
|
|
129
|
+
steps = [{"type": "pointerMove", "duration": 0, "x": x, "y": y}, {"type": "pointerDown"},
|
|
130
|
+
{"type": "pause", "duration": command["holdMs"]}, {"type": "pointerUp"}]
|
|
131
|
+
else:
|
|
132
|
+
steps = [{"type": "pointerMove", "duration": 0, "x": round(command["x1"] * width),
|
|
133
|
+
"y": round(command["y1"] * height)}, {"type": "pointerDown"},
|
|
134
|
+
{"type": "pointerMove", "duration": command["durationMs"], "x": round(command["x2"] * width),
|
|
135
|
+
"y": round(command["y2"] * height)}, {"type": "pointerUp"}]
|
|
136
|
+
driver.call("POST", "/actions", {"actions": [{"type": "pointer", "id": "finger",
|
|
137
|
+
"parameters": {"pointerType": "touch"}, "actions": steps}]},
|
|
138
|
+
timeout=10 + (command.get("holdMs", 0) + command.get("durationMs", 0)) / 1000)
|
|
139
|
+
|
|
140
|
+
def close(self):
|
|
141
|
+
driver, self.driver, self.session = self.driver, None, None
|
|
142
|
+
if driver is not None:
|
|
143
|
+
driver.close()
|
mobile_agent/maps.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Places on a map, for a result's ``map`` block and a table's Map view (docs/results.mdx).
|
|
2
|
+
|
|
3
|
+
``snapshot(places, ...)`` draws them on an Apple Maps snapshot made on this Mac by the ``map`` helper
|
|
4
|
+
(helper_map.swift: MapKit off screen, no window): {"image": PNG bytes at 2x, "width", "height" (points), "places":
|
|
5
|
+
[{"id", "lat", "lon", "x", "y"}], "missing": [ids]}. A place that has no coordinates is found by its address, then
|
|
6
|
+
by its name near ``near``, with Apple's geocoder. The page draws its own numbered pins over the picture (x and y are
|
|
7
|
+
where each one goes), so a pin is a button that names its place and says where it is.
|
|
8
|
+
|
|
9
|
+
Snapshots are kept on disk by what they show (the places, the appearance and the size), so a result opened again
|
|
10
|
+
draws at once and asks Apple's geocoder nothing. What reaches the helper is a place's name and address or
|
|
11
|
+
coordinates: the same words the result already shows.
|
|
12
|
+
|
|
13
|
+
``places_of(block)``: a ``map`` block's places, or a table's rows as places (a Name column, an Address or Location
|
|
14
|
+
column, or Latitude and Longitude columns).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
import tempfile
|
|
22
|
+
import threading
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
SECONDS = 45
|
|
26
|
+
KEEP = 200
|
|
27
|
+
MAX_PLACES = 30
|
|
28
|
+
_lock = threading.BoundedSemaphore(2)
|
|
29
|
+
|
|
30
|
+
NAME = re.compile(r"^(name|place|title|restaurant|store|shop|venue|business|cafe|café|spot)$", re.I)
|
|
31
|
+
ADDRESS = re.compile(r"^(address|location|where|street|addr\.?)$", re.I)
|
|
32
|
+
LAT = re.compile(r"^(lat|latitude)$", re.I)
|
|
33
|
+
LON = re.compile(r"^(lon|lng|long|longitude)$", re.I)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MapError(RuntimeError):
|
|
37
|
+
"""The map couldn't be drawn; the message says why, in plain words."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _column(columns, pattern):
|
|
41
|
+
return next((c["key"] for c in columns if pattern.match(str(c.get("label") or "").strip())), None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def places_of(block):
|
|
45
|
+
"""[{"id", "name", "address"?, "lat"?, "lon"?}] for a map block or a table with places, else []."""
|
|
46
|
+
if not isinstance(block, dict):
|
|
47
|
+
return []
|
|
48
|
+
if block.get("type") == "map":
|
|
49
|
+
return [{key: place[key] for key in ("id", "name", "address", "lat", "lon") if key in place}
|
|
50
|
+
for place in block.get("places") or ()][:MAX_PLACES]
|
|
51
|
+
if block.get("type") != "table":
|
|
52
|
+
return []
|
|
53
|
+
columns = block.get("columns") or []
|
|
54
|
+
address, lat, lon = _column(columns, ADDRESS), _column(columns, LAT), _column(columns, LON)
|
|
55
|
+
if address is None and (lat is None or lon is None):
|
|
56
|
+
return []
|
|
57
|
+
name = _column(columns, NAME) or next((c["key"] for c in columns if c["key"] not in (address, lat, lon)), None)
|
|
58
|
+
places = []
|
|
59
|
+
for index, row in enumerate(block.get("rows") or ()):
|
|
60
|
+
cells = row.get("cells") or {}
|
|
61
|
+
place = {"id": f"r{index}", "name": str(cells.get(name) or f"Place {index + 1}")[:120]}
|
|
62
|
+
if address is not None and isinstance(cells.get(address), str) and cells[address].strip():
|
|
63
|
+
place["address"] = cells[address].strip()[:200]
|
|
64
|
+
if lat is not None and lon is not None:
|
|
65
|
+
try:
|
|
66
|
+
y, x = float(cells.get(lat)), float(cells.get(lon))
|
|
67
|
+
if -90 <= y <= 90 and -180 <= x <= 180:
|
|
68
|
+
place["lat"], place["lon"] = y, x
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
pass
|
|
71
|
+
if "address" in place or "lat" in place:
|
|
72
|
+
places.append(place)
|
|
73
|
+
if len(places) >= MAX_PLACES:
|
|
74
|
+
break
|
|
75
|
+
return places
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _cache_dir():
|
|
79
|
+
from .paths import user_data_dir
|
|
80
|
+
folder = Path(user_data_dir()) / "cache" / "maps"
|
|
81
|
+
folder.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
82
|
+
return folder
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _prune(folder):
|
|
86
|
+
files = sorted(folder.glob("*.png"), key=lambda path: path.stat().st_mtime)
|
|
87
|
+
for old in files[:-KEEP]:
|
|
88
|
+
for path in (old, old.with_suffix(".json")):
|
|
89
|
+
path.unlink(missing_ok=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def snapshot(places, *, near=None, dark=False, width=720, height=360, helper=None, cache_dir=None,
|
|
93
|
+
timeout=SECONDS):
|
|
94
|
+
"""The places on a map (see the module docstring). MapError when there's nothing to draw or no helper."""
|
|
95
|
+
places = [p for p in places or () if isinstance(p, dict) and p.get("name")][:MAX_PLACES]
|
|
96
|
+
if not places:
|
|
97
|
+
raise MapError("There are no places to show on a map.")
|
|
98
|
+
width, height = max(200, min(1200, int(width))), max(120, min(900, int(height)))
|
|
99
|
+
request = {"places": places, "near": near, "width": width, "height": height, "dark": bool(dark)}
|
|
100
|
+
key = hashlib.sha256(json.dumps(request, sort_keys=True).encode()).hexdigest()[:24]
|
|
101
|
+
folder = Path(cache_dir) if cache_dir is not None else _cache_dir()
|
|
102
|
+
image, meta = folder / f"{key}.png", folder / f"{key}.json"
|
|
103
|
+
if image.is_file() and meta.is_file():
|
|
104
|
+
try:
|
|
105
|
+
return {**json.loads(meta.read_text()), "image": image.read_bytes()}
|
|
106
|
+
except (OSError, ValueError):
|
|
107
|
+
pass
|
|
108
|
+
if helper is None:
|
|
109
|
+
from . import native_helpers
|
|
110
|
+
helper = native_helpers.find("map")
|
|
111
|
+
if helper is None:
|
|
112
|
+
try:
|
|
113
|
+
helper = native_helpers.build("map")
|
|
114
|
+
except (LookupError, OSError, subprocess.SubprocessError) as error:
|
|
115
|
+
raise MapError("Mobster can't draw maps on this Mac: " + str(error)) from None
|
|
116
|
+
if not _lock.acquire(timeout=timeout):
|
|
117
|
+
raise MapError("Mobster is drawing other maps. Try again in a moment.")
|
|
118
|
+
try:
|
|
119
|
+
with tempfile.TemporaryDirectory(prefix="mobster-map-") as work:
|
|
120
|
+
source, target = Path(work) / "places.json", Path(work) / "map.png"
|
|
121
|
+
source.write_text(json.dumps(request), encoding="utf-8")
|
|
122
|
+
try:
|
|
123
|
+
done = subprocess.run([str(helper), str(source), str(target)], capture_output=True, text=True,
|
|
124
|
+
timeout=timeout)
|
|
125
|
+
except subprocess.TimeoutExpired:
|
|
126
|
+
raise MapError("Mobster took too long to draw the map. Try again.") from None
|
|
127
|
+
except OSError as error:
|
|
128
|
+
raise MapError(f"Mobster couldn't draw the map: {error}") from None
|
|
129
|
+
if done.returncode != 0 or not target.is_file():
|
|
130
|
+
lines = (done.stderr or "").strip().splitlines()
|
|
131
|
+
said = next((l for l in reversed(lines) if l.startswith("Mobster")), "Mobster couldn't draw the map")
|
|
132
|
+
raise MapError(said if said.endswith(".") else said + ".")
|
|
133
|
+
try:
|
|
134
|
+
answer = json.loads((done.stdout or "").strip().splitlines()[-1])
|
|
135
|
+
except (ValueError, IndexError):
|
|
136
|
+
raise MapError("Mobster couldn't read where the places are.") from None
|
|
137
|
+
data = target.read_bytes()
|
|
138
|
+
finally:
|
|
139
|
+
_lock.release()
|
|
140
|
+
out = {"width": answer.get("width", width), "height": answer.get("height", height),
|
|
141
|
+
"places": [p for p in answer.get("places") or () if isinstance(p, dict)],
|
|
142
|
+
"missing": [m for m in answer.get("missing") or () if isinstance(m, str)]}
|
|
143
|
+
try:
|
|
144
|
+
image.write_bytes(data)
|
|
145
|
+
meta.write_text(json.dumps(out))
|
|
146
|
+
_prune(folder)
|
|
147
|
+
except OSError:
|
|
148
|
+
pass
|
|
149
|
+
return {**out, "image": data}
|
mobile_agent/marks.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Set-of-Mark: numbered boxes on the screenshot, one per element a decision model may pick.
|
|
2
|
+
|
|
3
|
+
A decision model answers "which mark" and "which gesture"; code turns the mark back into the exact element (its
|
|
4
|
+
AX frame's centre when it taps), so the model never writes a coordinate.
|
|
5
|
+
|
|
6
|
+
``Mark`` keeps the element; ``mark_lines`` is the same list as text, for a text-only decider (Jev) and so both
|
|
7
|
+
deciders get identical options.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
TARGET_ROLES = frozenset({"Button", "Cell", "Link", "TextField", "SearchField", "SecureTextField", "TextView",
|
|
14
|
+
"Switch", "Toggle", "Tab", "TabBar", "MenuItem", "Image", "Icon", "SegmentedControl",
|
|
15
|
+
"Key", "PickerWheel", "Slider", "Stepper", "Other", "StaticText"})
|
|
16
|
+
MAX_MARKS = 120
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Mark:
|
|
21
|
+
id: str
|
|
22
|
+
element: object
|
|
23
|
+
rect: tuple
|
|
24
|
+
label: str
|
|
25
|
+
role: str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def center(self):
|
|
29
|
+
x, y, w, h = self.rect
|
|
30
|
+
return x + w / 2, y + h / 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def element_marks(elements, limit=MAX_MARKS):
|
|
34
|
+
"""Marks for the targetable elements on screen, top to bottom, ids "1", "2"...; at most ``limit``."""
|
|
35
|
+
out = []
|
|
36
|
+
for element in elements:
|
|
37
|
+
if len(out) >= limit:
|
|
38
|
+
break
|
|
39
|
+
x, y, w, h = element.rect
|
|
40
|
+
if w <= 0 or h <= 0 or y >= 1 or y + h <= 0:
|
|
41
|
+
continue
|
|
42
|
+
if element.role not in TARGET_ROLES and not getattr(element, "editable", False):
|
|
43
|
+
continue
|
|
44
|
+
out.append(Mark(str(len(out) + 1), element, tuple(element.rect), (element.label or "")[:120], element.role))
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def mark_lines(marks):
|
|
49
|
+
"""The marks as text: ``7 Button "Search" @4,14,92,5`` (frame in percent)."""
|
|
50
|
+
lines = []
|
|
51
|
+
for mark in marks:
|
|
52
|
+
x, y, w, h = (round(v * 100) for v in mark.rect)
|
|
53
|
+
label = f'"{mark.label}"' if mark.label else "(no label)"
|
|
54
|
+
value = getattr(mark.element, "value", "")
|
|
55
|
+
value = f" value={str(value)[:40]!r}" if value and value != mark.label else ""
|
|
56
|
+
lines.append(f"{mark.id} {mark.role} {label}{value} @{x},{y},{w},{h}")
|
|
57
|
+
return lines
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def options(marks, *, abstain=True):
|
|
61
|
+
"""A choice question's options for these marks (value = mark id), plus "none"."""
|
|
62
|
+
out = {mark.id: line for mark, line in zip(marks, mark_lines(marks))}
|
|
63
|
+
if abstain:
|
|
64
|
+
out["none"] = "None of the marked elements is right"
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def draw(jpeg, marks, side=None):
|
|
69
|
+
"""The screenshot (JPEG bytes) with each mark's box and number drawn; JPEG bytes back."""
|
|
70
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
71
|
+
image = Image.open(io.BytesIO(jpeg)).convert("RGB")
|
|
72
|
+
if side and image.width > side:
|
|
73
|
+
image = image.resize((side, round(image.height * side / image.width)))
|
|
74
|
+
canvas = ImageDraw.Draw(image)
|
|
75
|
+
try:
|
|
76
|
+
font = ImageFont.load_default(size=max(10, image.width // 36))
|
|
77
|
+
except TypeError:
|
|
78
|
+
font = ImageFont.load_default()
|
|
79
|
+
W, H = image.size
|
|
80
|
+
for mark in marks:
|
|
81
|
+
x, y, w, h = mark.rect
|
|
82
|
+
box = (int(x * W), int(y * H), int((x + w) * W), int((y + h) * H))
|
|
83
|
+
colour = (255, 40, 120)
|
|
84
|
+
canvas.rectangle(box, outline=colour, width=2)
|
|
85
|
+
text = mark.id
|
|
86
|
+
tw, th = canvas.textbbox((0, 0), text, font=font)[2:]
|
|
87
|
+
tag = (box[0], max(0, box[1] - th - 2), box[0] + tw + 4, max(th + 2, box[1]))
|
|
88
|
+
canvas.rectangle(tag, fill=colour)
|
|
89
|
+
canvas.text((tag[0] + 2, tag[1]), text, fill=(255, 255, 255), font=font)
|
|
90
|
+
out = io.BytesIO()
|
|
91
|
+
image.save(out, "JPEG", quality=70)
|
|
92
|
+
return out.getvalue()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""`python -m mobile_agent.mcp_server`: the same server as `mobster mcp`, with the same flags."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
DESCRIPTION = (
|
|
7
|
+
"Serve Mobster's tools to a coding agent over MCP (stdio). The agent starts a check\n"
|
|
8
|
+
"with verify_start, drives the app on a headless simulator, and gets the verdict\n"
|
|
9
|
+
"from verify_finish. With an OpenAI or Anthropic key, smart runs the steps itself\n"
|
|
10
|
+
"(Smart), on the model Smart picks for that key or the one MOBSTER_SMART_MODEL names.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser():
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .cli import add_arguments
|
|
17
|
+
parser = argparse.ArgumentParser(prog="python -m mobile_agent.mcp_server", description=DESCRIPTION,
|
|
18
|
+
formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
|
|
19
|
+
add_arguments(parser, {"path_type": lambda value: Path(value).expanduser()})
|
|
20
|
+
return parser
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv=None):
|
|
24
|
+
from .cli import run
|
|
25
|
+
return run(build_parser().parse_args(argv))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
sys.exit(main())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Durable intent and outcome for direct MCP mutations, including key-less checks."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from ..journal import append_jsonl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@contextmanager
|
|
11
|
+
def record_action(run_dir, operation):
|
|
12
|
+
# Arguments can contain passwords, codes and private URLs. Record only the
|
|
13
|
+
# operation here; the existing step records own their redacted descriptions.
|
|
14
|
+
path = Path(run_dir) / "actions.jsonl"
|
|
15
|
+
action = {"id": uuid4().hex, "operation": operation}
|
|
16
|
+
append_jsonl(path, {"event": "action_started", **action})
|
|
17
|
+
try:
|
|
18
|
+
yield
|
|
19
|
+
except BaseException:
|
|
20
|
+
# A timeout or failed screen read cannot prove that nothing happened.
|
|
21
|
+
append_jsonl(path, {"event": "action_finished", **action, "outcome": "unknown"})
|
|
22
|
+
raise
|
|
23
|
+
else:
|
|
24
|
+
append_jsonl(path, {"event": "action_finished", **action, "outcome": "acknowledged"})
|