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,312 @@
|
|
|
1
|
+
"""Static audits of a built app's bundle: they read files only, never launch anything and never change the bundle."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import plistlib
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
__all__ = ["Finding", "AUDITS", "run_audit", "bundle_symbols", "USAGE_KEYS", "REQUIRED_REASON"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Finding:
|
|
14
|
+
id: str # stable, such as "usage-description-missing"
|
|
15
|
+
guideline: str # "5.1.1(i)"
|
|
16
|
+
level: str # "fail" | "review"
|
|
17
|
+
message: str # what is wrong, then what to do
|
|
18
|
+
evidence: str # a path inside the bundle, with the key
|
|
19
|
+
|
|
20
|
+
def to_dict(self):
|
|
21
|
+
return {"id": self.id, "guideline": self.guideline, "level": self.level, "message": self.message,
|
|
22
|
+
"evidence": self.evidence}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MACHO_MAGICS = (b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe", b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca")
|
|
26
|
+
READ_LIMIT = 200 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _machos(app):
|
|
30
|
+
"""Every Mach-O file in the bundle: the executable, frameworks and plug-ins, as (relative path, path)."""
|
|
31
|
+
found = []
|
|
32
|
+
for folder, _, names in os.walk(app):
|
|
33
|
+
for name in names:
|
|
34
|
+
path = Path(folder) / name
|
|
35
|
+
if path.is_symlink() or not path.is_file():
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
with open(path, "rb") as handle:
|
|
39
|
+
head = handle.read(4)
|
|
40
|
+
except OSError:
|
|
41
|
+
continue
|
|
42
|
+
if head in MACHO_MAGICS:
|
|
43
|
+
found.append((path.relative_to(app).as_posix(), path))
|
|
44
|
+
return sorted(found)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _present(name, data):
|
|
48
|
+
# Swift mangles class and constant names inside longer ones; a C function is a whole NUL-delimited string.
|
|
49
|
+
word = name.encode()
|
|
50
|
+
if name[:1].isupper():
|
|
51
|
+
return word in data
|
|
52
|
+
return b"\0_" + word + b"\0" in data or b"\0" + word + b"\0" in data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def bundle_symbols(app, names):
|
|
56
|
+
"""{relative path of a Mach-O file: the set of ``names`` it holds} for the whole bundle."""
|
|
57
|
+
out = {}
|
|
58
|
+
for relative, path in _machos(Path(app)):
|
|
59
|
+
try:
|
|
60
|
+
if path.stat().st_size > READ_LIMIT:
|
|
61
|
+
continue
|
|
62
|
+
data = path.read_bytes()
|
|
63
|
+
except OSError:
|
|
64
|
+
continue
|
|
65
|
+
found = {name for name in names if _present(name, data)}
|
|
66
|
+
if found:
|
|
67
|
+
out[relative] = found
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def uses(symbols, names):
|
|
72
|
+
"""{Mach-O file: the first of ``names`` it holds} over the scan ``symbols`` (bundle_symbols)."""
|
|
73
|
+
hits = {}
|
|
74
|
+
for relative, found in sorted(symbols.items()):
|
|
75
|
+
for name in names:
|
|
76
|
+
if name in found:
|
|
77
|
+
hits[relative] = name
|
|
78
|
+
break
|
|
79
|
+
return hits
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# The Info.plist key each API needs, and the names whose presence in a binary means the app can reach it.
|
|
83
|
+
USAGE_KEYS = (
|
|
84
|
+
("NSCameraUsageDescription", ("AVCaptureDeviceInput", "AVCaptureSession", "VNDocumentCameraViewController")),
|
|
85
|
+
("NSMicrophoneUsageDescription", ("AVAudioRecorder", "AVAudioApplication")),
|
|
86
|
+
("NSPhotoLibraryUsageDescription", ("PHPhotoLibrary",)),
|
|
87
|
+
("NSPhotoLibraryAddUsageDescription", ("UIImageWriteToSavedPhotosAlbum",)),
|
|
88
|
+
("NSLocationWhenInUseUsageDescription", ("CLLocationManager",)),
|
|
89
|
+
("NSContactsUsageDescription", ("CNContactStore",)),
|
|
90
|
+
("NSCalendarsUsageDescription", ("EKEventStore",)),
|
|
91
|
+
("NSFaceIDUsageDescription", ("LAContext",)),
|
|
92
|
+
("NSBluetoothAlwaysUsageDescription", ("CBCentralManager", "CBPeripheralManager")),
|
|
93
|
+
("NSMotionUsageDescription", ("CMMotionActivityManager", "CMPedometer")),
|
|
94
|
+
("NSSpeechRecognitionUsageDescription", ("SFSpeechRecognizer",)),
|
|
95
|
+
("NSUserTrackingUsageDescription", ("ATTrackingManager",)),
|
|
96
|
+
("NSHealthShareUsageDescription", ("HKHealthStore",)),
|
|
97
|
+
)
|
|
98
|
+
ALSO_ACCEPTED = {
|
|
99
|
+
"NSLocationWhenInUseUsageDescription": ("NSLocationAlwaysAndWhenInUseUsageDescription",
|
|
100
|
+
"NSLocationAlwaysUsageDescription"),
|
|
101
|
+
"NSCalendarsUsageDescription": ("NSCalendarsFullAccessUsageDescription",
|
|
102
|
+
"NSCalendarsWriteOnlyAccessUsageDescription",
|
|
103
|
+
"NSRemindersUsageDescription", "NSRemindersFullAccessUsageDescription"),
|
|
104
|
+
"NSPhotoLibraryAddUsageDescription": ("NSPhotoLibraryUsageDescription",),
|
|
105
|
+
"NSHealthShareUsageDescription": ("NSHealthUpdateUsageDescription",),
|
|
106
|
+
}
|
|
107
|
+
PLACEHOLDER = re.compile(
|
|
108
|
+
r"lorem ipsum|\btodo\b|\btbd\b|\bfixme\b|placeholder|\bxxx+\b|usage description|your (?:text|description) here|"
|
|
109
|
+
r"enter (?:a |your )?(?:description|text|reason) here|^(?:test|testing|foo|bar|asdf|description|text|none|n/a|"
|
|
110
|
+
r"camera|microphone|location|photos|contacts|calendar)\.?$", re.I)
|
|
111
|
+
MIN_WORDS = 3
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_plist(path):
|
|
115
|
+
"""A property list as a dict (None when the file is missing, unreadable or not a dict)."""
|
|
116
|
+
try:
|
|
117
|
+
value = plistlib.loads(Path(path).read_bytes())
|
|
118
|
+
except Exception:
|
|
119
|
+
return None
|
|
120
|
+
return value if isinstance(value, dict) else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def placeholder_reason(text):
|
|
124
|
+
"""Why a usage string reads as filler (None when it reads as a real reason)."""
|
|
125
|
+
value = (text or "").strip()
|
|
126
|
+
if not value:
|
|
127
|
+
return "it is empty"
|
|
128
|
+
if PLACEHOLDER.search(value):
|
|
129
|
+
return "it reads as placeholder text"
|
|
130
|
+
if len(value.split()) < MIN_WORDS:
|
|
131
|
+
return f"it is {len(value.split())} word{'' if len(value.split()) == 1 else 's'}, too short to say why"
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def audit_usage_descriptions(app, ctx):
|
|
136
|
+
info = read_plist(Path(app) / "Info.plist")
|
|
137
|
+
if info is None:
|
|
138
|
+
return [Finding("info-plist-unreadable", "2.1", "review",
|
|
139
|
+
"The app's Info.plist can't be read. Build the app again and point the check at it.",
|
|
140
|
+
f"{Path(app).name}/Info.plist")]
|
|
141
|
+
symbols = ctx.symbols()
|
|
142
|
+
findings, plist = [], f"{Path(app).name}/Info.plist"
|
|
143
|
+
for key, names in USAGE_KEYS:
|
|
144
|
+
hits = uses(symbols, names)
|
|
145
|
+
if not hits:
|
|
146
|
+
continue
|
|
147
|
+
accepted = (key, *ALSO_ACCEPTED.get(key, ()))
|
|
148
|
+
present = [name for name in accepted if name in info]
|
|
149
|
+
where = next(iter(hits.items()))
|
|
150
|
+
if not present:
|
|
151
|
+
findings.append(Finding(
|
|
152
|
+
"usage-description-missing", "5.1.1", "fail",
|
|
153
|
+
f"{where[0]} uses {where[1]} and Info.plist has no {key}. Add {key} with the reason you ask.",
|
|
154
|
+
f"{plist} has no {key}; {where[1]} is in {where[0]}"))
|
|
155
|
+
continue
|
|
156
|
+
for name in present:
|
|
157
|
+
why = placeholder_reason(info.get(name) if isinstance(info.get(name), str) else "")
|
|
158
|
+
if why:
|
|
159
|
+
findings.append(Finding(
|
|
160
|
+
"usage-description-placeholder", "5.1.1", "fail",
|
|
161
|
+
f"{name} is {info.get(name)!r}: {why}. Write the reason a person would accept.",
|
|
162
|
+
f"{plist} {name}"))
|
|
163
|
+
# A key with filler even when no symbol showed the API: a framework may load it at run time.
|
|
164
|
+
for name, value in sorted(info.items()):
|
|
165
|
+
if name.endswith("UsageDescription") and isinstance(value, str):
|
|
166
|
+
why = placeholder_reason(value)
|
|
167
|
+
if why and not any(f.evidence == f"{plist} {name}" for f in findings):
|
|
168
|
+
findings.append(Finding(
|
|
169
|
+
"usage-description-placeholder", "5.1.1", "fail",
|
|
170
|
+
f"{name} is {value!r}: {why}. Write the reason a person would accept.", f"{plist} {name}"))
|
|
171
|
+
return findings
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# "weak" names are libc calls a binary can carry for other reasons: an undeclared one needs review, not a failure.
|
|
175
|
+
REQUIRED_REASON = {
|
|
176
|
+
"NSPrivacyAccessedAPICategoryFileTimestamp": {
|
|
177
|
+
"sure": ("NSFileCreationDate", "NSFileModificationDate", "NSURLContentModificationDateKey",
|
|
178
|
+
"NSURLCreationDateKey", "NSURLContentAccessDateKey", "getattrlist", "getattrlistbulk",
|
|
179
|
+
"fgetattrlist"),
|
|
180
|
+
"weak": ("stat", "fstat", "lstat", "fstatat")},
|
|
181
|
+
"NSPrivacyAccessedAPICategorySystemBootTime": {
|
|
182
|
+
"sure": ("systemUptime", "mach_absolute_time"), "weak": ()},
|
|
183
|
+
"NSPrivacyAccessedAPICategoryDiskSpace": {
|
|
184
|
+
"sure": ("NSURLVolumeAvailableCapacityKey", "NSURLVolumeAvailableCapacityForImportantUsageKey",
|
|
185
|
+
"NSURLVolumeTotalCapacityKey", "NSFileSystemFreeSize", "NSFileSystemSize", "statfs", "statvfs",
|
|
186
|
+
"fstatfs", "fstatvfs"), "weak": ()},
|
|
187
|
+
"NSPrivacyAccessedAPICategoryActiveKeyboards": {"sure": ("activeInputModes",), "weak": ()},
|
|
188
|
+
"NSPrivacyAccessedAPICategoryUserDefaults": {"sure": ("NSUserDefaults",), "weak": ()},
|
|
189
|
+
}
|
|
190
|
+
MANIFEST = "PrivacyInfo.xcprivacy"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def declared_categories(manifest):
|
|
194
|
+
"""({category: [reasons]} the manifest declares, a problem sentence or None)."""
|
|
195
|
+
types = manifest.get("NSPrivacyAccessedAPITypes")
|
|
196
|
+
if types is None:
|
|
197
|
+
return {}, None
|
|
198
|
+
if not isinstance(types, list):
|
|
199
|
+
return {}, "NSPrivacyAccessedAPITypes is not a list"
|
|
200
|
+
out = {}
|
|
201
|
+
for item in types:
|
|
202
|
+
if isinstance(item, dict) and isinstance(item.get("NSPrivacyAccessedAPIType"), str):
|
|
203
|
+
reasons = item.get("NSPrivacyAccessedAPITypeReasons")
|
|
204
|
+
out[item["NSPrivacyAccessedAPIType"]] = [r for r in reasons if isinstance(r, str)] \
|
|
205
|
+
if isinstance(reasons, list) else []
|
|
206
|
+
return out, None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def audit_privacy_manifest(app, ctx):
|
|
210
|
+
app = Path(app)
|
|
211
|
+
symbols = ctx.symbols()
|
|
212
|
+
needed = {}
|
|
213
|
+
for category, groups in REQUIRED_REASON.items():
|
|
214
|
+
for strength in ("sure", "weak"):
|
|
215
|
+
hits = uses(symbols, groups[strength])
|
|
216
|
+
if hits:
|
|
217
|
+
needed.setdefault(category, {})[strength] = hits
|
|
218
|
+
top = app / MANIFEST
|
|
219
|
+
findings = []
|
|
220
|
+
if not top.is_file():
|
|
221
|
+
if needed:
|
|
222
|
+
first = ", ".join(f"{category} ({next(iter((hits.get('sure') or hits['weak']).items()))[1]} in "
|
|
223
|
+
f"{next(iter(sorted((hits.get('sure') or hits['weak']))))})"
|
|
224
|
+
for category, hits in sorted(needed.items()))
|
|
225
|
+
findings.append(Finding(
|
|
226
|
+
"privacy-manifest-missing", "5.1.1", "fail",
|
|
227
|
+
f"{app.name} has no {MANIFEST} and its binaries use {len(needed)} required-reason API "
|
|
228
|
+
f"categor{'y' if len(needed) == 1 else 'ies'}. Add the file to the app target and declare each "
|
|
229
|
+
"with a reason.", f"{app.name}/{MANIFEST} is missing; uses {first}"))
|
|
230
|
+
else:
|
|
231
|
+
findings.append(Finding(
|
|
232
|
+
"privacy-manifest-missing", "5.1.1", "review",
|
|
233
|
+
f"{app.name} has no {MANIFEST}. No required-reason API shows in the binaries, so App Review "
|
|
234
|
+
"may accept it; add an empty manifest to be sure.", f"{app.name}/{MANIFEST} is missing"))
|
|
235
|
+
return findings
|
|
236
|
+
manifest = read_plist(top)
|
|
237
|
+
if manifest is None:
|
|
238
|
+
return [Finding("privacy-manifest-invalid", "5.1.1", "fail",
|
|
239
|
+
f"{MANIFEST} is not a valid property list. Open it in Xcode and save it again.",
|
|
240
|
+
f"{app.name}/{MANIFEST}")]
|
|
241
|
+
declared, problem = declared_categories(manifest)
|
|
242
|
+
if problem:
|
|
243
|
+
findings.append(Finding("privacy-manifest-invalid", "5.1.1", "fail",
|
|
244
|
+
f"{problem}. Open {MANIFEST} in Xcode and fix the key.", f"{app.name}/{MANIFEST}"))
|
|
245
|
+
for category, reasons in sorted(declared.items()):
|
|
246
|
+
if not reasons:
|
|
247
|
+
findings.append(Finding(
|
|
248
|
+
"privacy-reason-missing", "5.1.1", "fail",
|
|
249
|
+
f"{category} is declared with no reason code. Add NSPrivacyAccessedAPITypeReasons.",
|
|
250
|
+
f"{app.name}/{MANIFEST} {category}"))
|
|
251
|
+
for category, strengths in sorted(needed.items()):
|
|
252
|
+
if category in declared:
|
|
253
|
+
continue
|
|
254
|
+
for strength, level in (("sure", "fail"), ("weak", "review")):
|
|
255
|
+
hits = strengths.get(strength) or {}
|
|
256
|
+
own = {f: n for f, n in hits.items() if not f.startswith(("Frameworks/", "PlugIns/"))}
|
|
257
|
+
if own:
|
|
258
|
+
file, name = next(iter(sorted(own.items())))
|
|
259
|
+
findings.append(Finding(
|
|
260
|
+
"required-reason-undeclared", "5.1.1", level,
|
|
261
|
+
f"{file} uses {name} and {MANIFEST} does not declare {category}. Add it with the reason "
|
|
262
|
+
"that fits how the app uses it.", f"{app.name}/{MANIFEST} lacks {category}; {name} is in {file}"))
|
|
263
|
+
break
|
|
264
|
+
embedded = {f: n for f, n in hits.items() if f not in own}
|
|
265
|
+
if embedded:
|
|
266
|
+
file, name = next(iter(sorted(embedded.items())))
|
|
267
|
+
folder = "/".join(file.split("/")[:2])
|
|
268
|
+
own_manifest = read_plist(app / folder / MANIFEST) or {}
|
|
269
|
+
if category in declared_categories(own_manifest)[0]:
|
|
270
|
+
continue
|
|
271
|
+
state = f"lacks {category}" if own_manifest else "is missing"
|
|
272
|
+
findings.append(Finding(
|
|
273
|
+
"required-reason-sdk", "5.1.1", "review",
|
|
274
|
+
f"{file} uses {name} and neither it nor {MANIFEST} declares {category}. Update the SDK "
|
|
275
|
+
"to a version with its own privacy manifest, or declare the category yourself.",
|
|
276
|
+
f"{folder}/{MANIFEST} {state}; {name} is in {file}"))
|
|
277
|
+
break
|
|
278
|
+
return findings
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class Context:
|
|
282
|
+
"""What the audits of one run share: the bundle's symbols, read once."""
|
|
283
|
+
|
|
284
|
+
def __init__(self, app):
|
|
285
|
+
self.app = Path(app)
|
|
286
|
+
self._symbols = None
|
|
287
|
+
|
|
288
|
+
def symbols(self):
|
|
289
|
+
if self._symbols is None:
|
|
290
|
+
names = {name for _, group in USAGE_KEYS for name in group}
|
|
291
|
+
for groups in REQUIRED_REASON.values():
|
|
292
|
+
names.update(groups["sure"])
|
|
293
|
+
names.update(groups["weak"])
|
|
294
|
+
self._symbols = bundle_symbols(self.app, sorted(names))
|
|
295
|
+
return self._symbols
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
AUDITS = {
|
|
299
|
+
"usage-descriptions": audit_usage_descriptions,
|
|
300
|
+
"privacy-manifest": audit_privacy_manifest,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def run_audit(name, app, *, context=None):
|
|
305
|
+
"""The findings of audit ``name`` on the built app ``app``. A bundle that isn't there is one review finding,
|
|
306
|
+
never a crash."""
|
|
307
|
+
app = Path(app)
|
|
308
|
+
if not app.is_dir():
|
|
309
|
+
return [Finding("app-not-found", "2.1", "review", f"{app} isn't a built app. Build it, then run again.",
|
|
310
|
+
str(app))]
|
|
311
|
+
ctx = context or Context(app)
|
|
312
|
+
return AUDITS[name](app, ctx)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""A bounded, locally enrolled app-state oracle for simulator checks.
|
|
2
|
+
|
|
3
|
+
The check names an adapter and fixture; only a project-owned manifest can define
|
|
4
|
+
their fields and seed values. This is persisted app state, not a backend oracle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import stat
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..state import validate_bundle_id
|
|
17
|
+
from .assertions import CheckError
|
|
18
|
+
|
|
19
|
+
CONTRACT = "mobster.business/1"
|
|
20
|
+
FILENAME = "mobster-business.json"
|
|
21
|
+
MANIFEST = ".mobster/fixtures.json"
|
|
22
|
+
_ID = re.compile(r"[a-z][a-z0-9_-]{0,63}\Z")
|
|
23
|
+
_FIELD = re.compile(r"[a-z][a-z0-9_.-]{0,79}\Z")
|
|
24
|
+
_MAX_BYTES = 64_000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class BusinessExpectation:
|
|
29
|
+
field: str
|
|
30
|
+
equals: object
|
|
31
|
+
|
|
32
|
+
def to_dict(self):
|
|
33
|
+
return {"field": self.field, "equals": self.equals}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class BusinessSpec:
|
|
38
|
+
adapter: str
|
|
39
|
+
fixture: str
|
|
40
|
+
expect: tuple[BusinessExpectation, ...]
|
|
41
|
+
|
|
42
|
+
def to_dict(self):
|
|
43
|
+
return {"adapter": self.adapter, "fixture": self.fixture,
|
|
44
|
+
"expect": [item.to_dict() for item in self.expect]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class BusinessBinding:
|
|
49
|
+
spec: BusinessSpec
|
|
50
|
+
bundle: str
|
|
51
|
+
fields: tuple[str, ...]
|
|
52
|
+
seed: tuple[tuple[str, object], ...]
|
|
53
|
+
digest: str
|
|
54
|
+
|
|
55
|
+
def seed_state(self):
|
|
56
|
+
return dict(self.seed)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def action_digest(self):
|
|
60
|
+
"""Replay starting state; expected assertions do not change the action path."""
|
|
61
|
+
encoded = json.dumps({"contract": CONTRACT, "adapter": self.spec.adapter, "fixture": self.spec.fixture,
|
|
62
|
+
"bundle": self.bundle, "fields": self.fields, "seed": self.seed_state()},
|
|
63
|
+
sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
64
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _id(value, where, pattern=_ID):
|
|
68
|
+
if not isinstance(value, str) or not pattern.fullmatch(value):
|
|
69
|
+
raise CheckError(f"{where} must be a lowercase symbolic ID")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _scalar(value, where):
|
|
74
|
+
if value is None or isinstance(value, bool):
|
|
75
|
+
return value
|
|
76
|
+
if isinstance(value, str) and len(value) <= 256 and all(
|
|
77
|
+
ord(c) >= 32 and not 127 <= ord(c) <= 159 and not 0xD800 <= ord(c) <= 0xDFFF
|
|
78
|
+
and not 0xFDD0 <= ord(c) <= 0xFDEF and ord(c) & 0xFFFF not in (0xFFFE, 0xFFFF)
|
|
79
|
+
for c in value):
|
|
80
|
+
return value
|
|
81
|
+
if isinstance(value, int) and abs(value) <= 1_000_000_000:
|
|
82
|
+
return value
|
|
83
|
+
if isinstance(value, float) and math.isfinite(value) and abs(value) <= 1_000_000_000:
|
|
84
|
+
return value
|
|
85
|
+
raise CheckError(f"{where} must be a bounded JSON scalar")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def valid_scalar(value):
|
|
89
|
+
try:
|
|
90
|
+
_scalar(value, "app-state value")
|
|
91
|
+
except CheckError:
|
|
92
|
+
return False
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_business(value):
|
|
97
|
+
if value is None:
|
|
98
|
+
return None
|
|
99
|
+
if not isinstance(value, dict) or set(value) != {"adapter", "fixture", "expect"}:
|
|
100
|
+
raise CheckError("business needs adapter, fixture and expect; no other keys")
|
|
101
|
+
adapter = _id(value["adapter"], "business.adapter")
|
|
102
|
+
fixture = _id(value["fixture"], "business.fixture")
|
|
103
|
+
items = value["expect"]
|
|
104
|
+
if not isinstance(items, list) or len(items) > 20:
|
|
105
|
+
raise CheckError("business.expect must be a list of at most 20 field comparisons")
|
|
106
|
+
seen = set()
|
|
107
|
+
parsed = []
|
|
108
|
+
for index, item in enumerate(items):
|
|
109
|
+
if not isinstance(item, dict) or set(item) != {"field", "equals"}:
|
|
110
|
+
raise CheckError(f"business.expect[{index}] needs field and equals")
|
|
111
|
+
field = _id(item["field"], f"business.expect[{index}].field", _FIELD)
|
|
112
|
+
if field in seen:
|
|
113
|
+
raise CheckError(f"business.expect repeats field {field!r}")
|
|
114
|
+
seen.add(field)
|
|
115
|
+
parsed.append(BusinessExpectation(field, _scalar(item["equals"], f"business.expect[{index}].equals")))
|
|
116
|
+
return BusinessSpec(adapter, fixture, tuple(parsed))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _pairs(pairs):
|
|
120
|
+
result = {}
|
|
121
|
+
for key, value in pairs:
|
|
122
|
+
if key in result:
|
|
123
|
+
raise CheckError(f"duplicate registration key {key!r}")
|
|
124
|
+
result[key] = value
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _invalid_constant(value):
|
|
129
|
+
raise CheckError(f"non-finite JSON value {value!r}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def strict_json(raw):
|
|
133
|
+
return json.loads(raw, object_pairs_hook=_pairs, parse_constant=_invalid_constant)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _manifest_bytes(project_root):
|
|
137
|
+
root = os.open(Path(project_root).resolve(), os.O_RDONLY | os.O_DIRECTORY)
|
|
138
|
+
try:
|
|
139
|
+
folder = os.open(".mobster", os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root)
|
|
140
|
+
try:
|
|
141
|
+
descriptor = os.open("fixtures.json", os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW, dir_fd=folder)
|
|
142
|
+
with os.fdopen(descriptor, "rb") as source:
|
|
143
|
+
info = os.fstat(source.fileno())
|
|
144
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > _MAX_BYTES:
|
|
145
|
+
raise CheckError(f"{MANIFEST} must be a regular file of at most {_MAX_BYTES:,} bytes")
|
|
146
|
+
raw = source.read(_MAX_BYTES + 1)
|
|
147
|
+
if len(raw) > _MAX_BYTES:
|
|
148
|
+
raise CheckError(f"{MANIFEST} is larger than {_MAX_BYTES:,} bytes")
|
|
149
|
+
return raw
|
|
150
|
+
finally:
|
|
151
|
+
os.close(folder)
|
|
152
|
+
finally:
|
|
153
|
+
os.close(root)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_business_binding(check, project_root):
|
|
157
|
+
"""Resolve a check against the project-owned, versioned local registration."""
|
|
158
|
+
spec = check.business
|
|
159
|
+
if spec is None:
|
|
160
|
+
raise CheckError("the check has no business binding")
|
|
161
|
+
try:
|
|
162
|
+
data = strict_json(_manifest_bytes(project_root))
|
|
163
|
+
except CheckError:
|
|
164
|
+
raise
|
|
165
|
+
except (OSError, UnicodeError, ValueError, RecursionError) as error:
|
|
166
|
+
raise CheckError(f"{MANIFEST} is missing or invalid ({type(error).__name__})") from None
|
|
167
|
+
if not isinstance(data, dict) or set(data) != {"version", "adapters"} or type(data["version"]) is not int \
|
|
168
|
+
or data["version"] != 1 or not isinstance(data["adapters"], dict) \
|
|
169
|
+
or len(data["adapters"]) > 16:
|
|
170
|
+
raise CheckError(f"{MANIFEST} needs version 1 and at most 16 adapters")
|
|
171
|
+
for adapter_id in data["adapters"]:
|
|
172
|
+
_id(adapter_id, "adapter registration")
|
|
173
|
+
registration = data["adapters"].get(spec.adapter)
|
|
174
|
+
if not isinstance(registration, dict) or set(registration) != {"kind", "bundle", "fields", "fixtures"} \
|
|
175
|
+
or registration["kind"] != "app_container_json":
|
|
176
|
+
raise CheckError(f"business adapter {spec.adapter!r} is not locally registered for app_container_json")
|
|
177
|
+
bundle = registration["bundle"]
|
|
178
|
+
try:
|
|
179
|
+
bundle = validate_bundle_id(bundle)
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
raise CheckError("the registered app-state bundle is not a bundle ID") from None
|
|
182
|
+
if check.bundle_id and bundle != check.bundle_id:
|
|
183
|
+
raise CheckError("the registered app-state bundle does not match the check")
|
|
184
|
+
fields = registration["fields"]
|
|
185
|
+
if not isinstance(fields, list) or not 1 <= len(fields) <= 20:
|
|
186
|
+
raise CheckError("business adapter fields must be 1–20 distinct keys")
|
|
187
|
+
for field in fields:
|
|
188
|
+
_id(field, "business adapter field", _FIELD)
|
|
189
|
+
if len(set(fields)) != len(fields):
|
|
190
|
+
raise CheckError("business adapter fields must be 1–20 distinct keys")
|
|
191
|
+
fixtures = registration["fixtures"]
|
|
192
|
+
if not isinstance(fixtures, dict) or not 1 <= len(fixtures) <= 20:
|
|
193
|
+
raise CheckError("business adapter needs 1–20 named fixtures")
|
|
194
|
+
for fixture_id in fixtures:
|
|
195
|
+
_id(fixture_id, "business fixture")
|
|
196
|
+
seed = fixtures.get(spec.fixture)
|
|
197
|
+
if not isinstance(seed, dict) or set(seed) != set(fields):
|
|
198
|
+
raise CheckError(f"business fixture {spec.fixture!r} is missing or has unregistered fields")
|
|
199
|
+
seed = tuple((field, _scalar(seed[field], f"business fixture {spec.fixture}.{field}")) for field in fields)
|
|
200
|
+
if any(item.field not in fields for item in spec.expect):
|
|
201
|
+
raise CheckError("business.expect names a field this adapter did not register")
|
|
202
|
+
canonical = json.dumps({"contract": CONTRACT, "adapter": spec.adapter, "fixture": spec.fixture,
|
|
203
|
+
"registration": registration, "expect": [item.to_dict() for item in spec.expect]},
|
|
204
|
+
sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
205
|
+
return BusinessBinding(spec, bundle, tuple(fields), seed, hashlib.sha256(canonical).hexdigest())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def evaluate_business(binding, state):
|
|
209
|
+
"""All comparisons use one app-produced snapshot; JSON types must match exactly."""
|
|
210
|
+
def equal(actual, expected):
|
|
211
|
+
if type(actual) is bool or type(expected) is bool:
|
|
212
|
+
return type(actual) is type(expected) and actual == expected
|
|
213
|
+
if type(actual) in (int, float) and type(expected) in (int, float):
|
|
214
|
+
return actual == expected
|
|
215
|
+
return type(actual) is type(expected) and actual == expected
|
|
216
|
+
|
|
217
|
+
return [{"field": item.field, "ok": item.field in state and equal(state[item.field], item.equals),
|
|
218
|
+
"observed": (state[item.field] if type(state[item.field]) is bool or state[item.field] is None
|
|
219
|
+
else f"[{type(state[item.field]).__name__} redacted]") if item.field in state else None,
|
|
220
|
+
"present": item.field in state}
|
|
221
|
+
for item in binding.spec.expect]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""One VerifyRun's registered app-state fixture and evidence lifecycle."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .assertions import CheckError
|
|
7
|
+
from .business import FILENAME, evaluate_business, load_business_binding
|
|
8
|
+
from .checks import project_root
|
|
9
|
+
from .verdict import CouldntRun
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _seeds_reserved_file(seed):
|
|
13
|
+
reserved = ("documents", FILENAME.casefold())
|
|
14
|
+
for source, destination in seed.files:
|
|
15
|
+
parts = tuple(part.casefold() for part in Path(destination).parts)
|
|
16
|
+
if parts[:2] == reserved:
|
|
17
|
+
return True
|
|
18
|
+
if not source.is_dir() or parts != reserved[:len(parts)]:
|
|
19
|
+
continue
|
|
20
|
+
current = source
|
|
21
|
+
for component in reserved[len(parts):]:
|
|
22
|
+
current = next((child for child in current.iterdir() if child.name.casefold() == component), None)
|
|
23
|
+
if current is None:
|
|
24
|
+
break
|
|
25
|
+
if current is not None:
|
|
26
|
+
return True
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BusinessRun:
|
|
31
|
+
def _load_business(self):
|
|
32
|
+
if self.check.business is None:
|
|
33
|
+
return
|
|
34
|
+
if self.check.reset_level() == "none":
|
|
35
|
+
raise CouldntRun("business", "App-state fixtures need reset: data or reinstall.",
|
|
36
|
+
"Set reset: data or reset: reinstall in the saved check.")
|
|
37
|
+
if self.check.source is None:
|
|
38
|
+
raise CouldntRun("business", "An app-state check must be a saved project check file.",
|
|
39
|
+
"Save the check under a project with .mobster/fixtures.json, then run again.")
|
|
40
|
+
try:
|
|
41
|
+
collision = _seeds_reserved_file(self.check.seed)
|
|
42
|
+
except OSError:
|
|
43
|
+
raise CouldntRun("business", "A YAML fixture's seed files could not be inspected before reset.",
|
|
44
|
+
"Fix the YAML fixture, then run the check again.") from None
|
|
45
|
+
if collision:
|
|
46
|
+
raise CouldntRun("business", "A YAML fixture seeds the reserved app-state file.",
|
|
47
|
+
"Remove that file from the YAML fixture before running this check.")
|
|
48
|
+
try:
|
|
49
|
+
current = load_business_binding(self.check, project_root(self.check.source))
|
|
50
|
+
except CheckError as error:
|
|
51
|
+
raise CouldntRun("business", str(error), "Fix .mobster/fixtures.json, then run again.") from None
|
|
52
|
+
if self.business_binding is not None and current.digest != self.business_binding.digest:
|
|
53
|
+
raise CouldntRun("business", "The enrolled app-state fixture changed before the run.",
|
|
54
|
+
"Re-enroll the project fixture, then run again.")
|
|
55
|
+
self.business_binding = current
|
|
56
|
+
self.business_result = {"adapter": current.spec.adapter, "fixture": current.spec.fixture,
|
|
57
|
+
"status": "unavailable", "schemaVersion": "mobster.business/1",
|
|
58
|
+
"bindingSha256": current.digest, "assertions": [],
|
|
59
|
+
"setup": "not_started", "cleanup": "not_needed"}
|
|
60
|
+
|
|
61
|
+
def _setup_business(self, manager):
|
|
62
|
+
if self.business_binding is None:
|
|
63
|
+
return
|
|
64
|
+
from .business_state import AppContainerFixture, FixtureError
|
|
65
|
+
self.business_fixture = AppContainerFixture(self.business_binding, manager, self.target, self.actions,
|
|
66
|
+
self.run_id, secrets.token_hex(16))
|
|
67
|
+
with self.actions.action("BUSINESS_QUIESCE", operation_id=f"{self.run_id}:business:preseed"):
|
|
68
|
+
self.business_fixture.quiesce()
|
|
69
|
+
self.business_result["setup"] = "pending"
|
|
70
|
+
try:
|
|
71
|
+
self.business_fixture.setup()
|
|
72
|
+
except (FixtureError, OSError) as error:
|
|
73
|
+
self.business_result["setup"] = "needs_reconciliation" if self.actions.blocked else "failed"
|
|
74
|
+
raise CouldntRun("business", f"The app-state fixture could not be seeded ({type(error).__name__}).",
|
|
75
|
+
"Inspect the fixture file and simulator, then start a new run.") from None
|
|
76
|
+
self.business_result["setup"] = "seeded"
|
|
77
|
+
|
|
78
|
+
def _take_business_baseline(self):
|
|
79
|
+
if self.business_fixture is None:
|
|
80
|
+
return
|
|
81
|
+
from .business_state import FixtureError
|
|
82
|
+
try:
|
|
83
|
+
state, generation, digest = self.business_fixture.observe()
|
|
84
|
+
except (FixtureError, OSError) as error:
|
|
85
|
+
raise CouldntRun("business_evidence", f"The app-state baseline is unavailable ({type(error).__name__}).",
|
|
86
|
+
"Check that the app publishes its persisted state after launch.") from None
|
|
87
|
+
self._business_baseline = (generation, digest)
|
|
88
|
+
self.baseline["businessHeld"] = [item["ok"] for item in evaluate_business(self.business_binding, state)]
|
|
89
|
+
|
|
90
|
+
def _observe_business(self, decided):
|
|
91
|
+
if self.business_fixture is None:
|
|
92
|
+
return None
|
|
93
|
+
from .business_state import FixtureError
|
|
94
|
+
try:
|
|
95
|
+
state, generation, digest = self.business_fixture.observe()
|
|
96
|
+
except (FixtureError, OSError) as error:
|
|
97
|
+
self.business_result["error"] = "App-produced state was missing, stale or malformed"
|
|
98
|
+
return {**decided, "verdict": "couldnt_run", "klass": "business_evidence",
|
|
99
|
+
"message": f"The app-state observation is unavailable ({type(error).__name__}).",
|
|
100
|
+
"fix": "Check the app's fixture producer and run a new check."}
|
|
101
|
+
if self._business_baseline is not None:
|
|
102
|
+
before_generation, before_digest = self._business_baseline
|
|
103
|
+
if generation < before_generation or generation == before_generation and digest != before_digest:
|
|
104
|
+
self.business_result["error"] = "The app-state generation moved backward or changed in place"
|
|
105
|
+
return {**decided, "verdict": "couldnt_run", "klass": "business_evidence",
|
|
106
|
+
"message": "The app-state observation has a stale generation.",
|
|
107
|
+
"fix": "Make the app increment generation for every persisted snapshot."}
|
|
108
|
+
self.business_result.update(status="observed", generation=generation, sourceSha256=digest,
|
|
109
|
+
assertions=evaluate_business(self.business_binding, state))
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def _cleanup_business(self):
|
|
113
|
+
if self.business_fixture is None or self._business_cleanup_done:
|
|
114
|
+
return None
|
|
115
|
+
self._business_cleanup_done = True
|
|
116
|
+
if self.actions.blocked or self.error is not None and self.error["klass"] == "unknown_outcome":
|
|
117
|
+
self.business_result["cleanup"] = "needs_reconciliation"
|
|
118
|
+
return None
|
|
119
|
+
try:
|
|
120
|
+
status = self.business_fixture.remove()
|
|
121
|
+
except BaseException as error:
|
|
122
|
+
self.business_result["cleanup"] = "needs_reconciliation"
|
|
123
|
+
failure = self._action_failure()
|
|
124
|
+
return failure or CouldntRun("business_cleanup",
|
|
125
|
+
f"The app-state fixture was not confirmed removed ({type(error).__name__}).",
|
|
126
|
+
"Inspect the simulator before another run.")
|
|
127
|
+
self.business_result["cleanup"] = status
|
|
128
|
+
if status == "needs_reconciliation":
|
|
129
|
+
return CouldntRun("business_cleanup", "The app-state fixture was not confirmed removed.",
|
|
130
|
+
"Inspect the simulator before another run.")
|
|
131
|
+
return None
|