@deeeed/metamask-harness 0.36.0 → 0.38.0

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/adapters/extension/artifact-runtime-state.cjs +138 -0
  3. package/adapters/extension/inject.mjs +2 -0
  4. package/adapters/extension/launch-browser.cjs +24 -0
  5. package/adapters/extension/live.sh +82 -12
  6. package/adapters/extension/readiness.mjs +77 -36
  7. package/adapters/extension/snapshot-dist.sh +88 -3
  8. package/adapters/extension/verify.sh +6 -2
  9. package/adapters/manifest.json +17 -1
  10. package/adapters/shared/log-tui.mjs +1 -1
  11. package/dist/adapters/extension/artifact-integrity.js +38 -0
  12. package/dist/adapters/extension/extension-id.js +23 -4
  13. package/dist/adapters/extension/release-artifact.js +386 -0
  14. package/dist/adapters/extension/runtime-decision.js +164 -20
  15. package/dist/adapters/extension/runtime.js +169 -3
  16. package/dist/adapters/mobile/provision.js +38 -3
  17. package/dist/adapters/mobile/release-artifact-state.js +124 -0
  18. package/dist/adapters/mobile/release-artifact.js +295 -0
  19. package/dist/adapters.js +78 -6
  20. package/dist/app-lifecycle.js +4 -3
  21. package/dist/command-contract.js +16 -0
  22. package/dist/commands/call.js +6 -2
  23. package/dist/commands/launch/extension.js +74 -9
  24. package/dist/commands/launch/index.js +3 -0
  25. package/dist/commands/launch/mobile.js +2 -0
  26. package/dist/commands/provision.js +2 -0
  27. package/dist/commands/run-engine.js +109 -14
  28. package/dist/commands/run.js +4 -2
  29. package/dist/commands/runtime-launch.js +178 -10
  30. package/dist/heal-bounds.js +1 -1
  31. package/dist/live-adapter-contract.js +3 -1
  32. package/dist/metamask-action-validation.js +47 -1
  33. package/dist/mm-harness-cli.js +31 -1
  34. package/dist/recipe-security.js +40 -0
  35. package/dist/run-diagnostics.js +1 -1
  36. package/dist/runner.js +3 -0
  37. package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
  38. package/library/actions/extension/assets/finish_send.mjs +128 -0
  39. package/library/actions/extension/assets/import_custom_token.mjs +227 -0
  40. package/library/actions/extension/assets/prepare_send.mjs +53 -0
  41. package/library/actions/extension/assets/read_details.mjs +57 -0
  42. package/library/actions/extension/assets/read_visible_state.mjs +50 -0
  43. package/library/actions/extension/assets/review_send.mjs +23 -0
  44. package/library/actions/extension/assets/select_network_scope.mjs +30 -0
  45. package/library/actions/extension/assets/send.mjs +104 -0
  46. package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
  47. package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
  48. package/library/actions/extension/assets/verify_sorting.mjs +89 -0
  49. package/library/actions/extension/networks/add_chainlist.mjs +94 -0
  50. package/library/actions/extension/networks/add_custom.mjs +123 -0
  51. package/library/actions/extension/networks/custom_network.mjs +103 -0
  52. package/library/actions/extension/networks/read_visible_state.mjs +50 -0
  53. package/library/actions/extension/networks/remove_custom.mjs +68 -0
  54. package/library/actions/extension/perps/close_orders.mjs +1 -0
  55. package/library/actions/extension/perps/close_visible_position.mjs +133 -0
  56. package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
  57. package/library/actions/extension/perps/edit_margin.mjs +82 -0
  58. package/library/actions/extension/perps/open_balance_action.mjs +58 -0
  59. package/library/actions/extension/perps/open_position_action.mjs +80 -0
  60. package/library/actions/extension/perps/perps.mjs +196 -2
  61. package/library/actions/extension/perps/place_order.mjs +5 -2
  62. package/library/actions/extension/perps/read_eligibility.mjs +34 -0
  63. package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
  64. package/library/actions/extension/perps/read_snapshot.mjs +470 -0
  65. package/library/actions/extension/perps/read_visible_state.mjs +98 -0
  66. package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
  67. package/library/actions/extension/perps/select_market_filter.mjs +71 -0
  68. package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
  69. package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
  70. package/library/actions/extension/platform/cdp.mjs +109 -3
  71. package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
  72. package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
  73. package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
  74. package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
  75. package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
  76. package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
  77. package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
  78. package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
  79. package/library/actions/extension/ui/navigate.mjs +264 -8
  80. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
  81. package/library/actions/extension/wallet/import.mjs +13 -46
  82. package/library/actions/extension/wallet/lock.mjs +35 -0
  83. package/library/actions/extension/wallet/secret-input.mjs +98 -0
  84. package/library/actions/extension/wallet/select_account.mjs +71 -7
  85. package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
  86. package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
  87. package/library/actions/mobile/platform/native-session-name.mjs +5 -0
  88. package/library/actions/mobile/platform/observe-ui.mjs +86 -2
  89. package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
  90. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
  91. package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
  92. package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
  93. package/library/actions/mobile/ui/native-navigation.mjs +265 -0
  94. package/library/actions/mobile/ui/navigate.mjs +10 -1
  95. package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
  96. package/library/actions/mobile/wallet/import.mjs +71 -2
  97. package/library/actions/mobile/wallet/native-ui.mjs +512 -0
  98. package/library/actions/mobile/wallet/read_state.mjs +16 -0
  99. package/library/actions/mobile/wallet/reset.mjs +17 -4
  100. package/library/actions/shared/assets/visible-state.mjs +176 -0
  101. package/library/actions/shared/networks/visible-state.mjs +70 -0
  102. package/library/actions/shared/perps/visible-state.mjs +354 -0
  103. package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
  104. package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
  105. package/library/actions/shared/ui/locators.mjs +7 -0
  106. package/library/manifests/extension.action-manifest.json +1366 -184
  107. package/library/manifests/mobile.action-manifest.json +235 -1
  108. package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
  109. package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
  110. package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
  111. package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
  112. package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
  113. package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
  114. package/library/recipes/assets/release-token-details.recipe.json +48 -0
  115. package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
  116. package/library/recipes/assets/release-token-list.recipe.json +75 -0
  117. package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
  118. package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
  119. package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
  120. package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
  121. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
  122. package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
  123. package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
  124. package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
  125. package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
  126. package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
  127. package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
  128. package/library/recipes/perps/release-market-details.recipe.json +83 -0
  129. package/library/recipes/perps/release-market-search.recipe.json +79 -0
  130. package/library/recipes/perps/release-watchlist.recipe.json +99 -0
  131. package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
  132. package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
  133. package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
  134. package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
  135. package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
  136. package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
  137. package/library/recipes/wallet/import.recipe.json +20 -1
  138. package/library/recipes/wallet/reset-import.recipe.json +20 -1
  139. package/package.json +6 -5
  140. package/scripts/validate-release-report.mjs +319 -0
package/dist/adapters.js CHANGED
@@ -12,10 +12,16 @@ import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-p
12
12
  import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
13
13
  const execFileAsync = promisify(execFile);
14
14
  const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
15
+ "ui.press",
16
+ "ui.key_press",
17
+ "ui.set_input",
18
+ "ui.scroll",
15
19
  "ui.swipe",
16
20
  "ui.pan",
17
21
  "ui.drag",
18
22
  "ui.long_press",
23
+ "ui.wait_for",
24
+ "ui.screenshot",
19
25
  "ui.capture_surface"
20
26
  ]);
21
27
  function sleep(ms) {
@@ -29,7 +35,42 @@ function simpleAdapter(action, executor) {
29
35
  }
30
36
  };
31
37
  }
32
- const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
38
+ const LIVE_ONLY_ACTIONS = /* @__PURE__ */ new Set([
39
+ "metamask.assets.read_visible_state",
40
+ "metamask.assets.read_details",
41
+ "metamask.assets.verify_sorting",
42
+ "metamask.assets.set_token_visibility",
43
+ "metamask.assets.import_custom_token",
44
+ "metamask.assets.prepare_send",
45
+ "metamask.assets.review_send",
46
+ "metamask.assets.finish_send",
47
+ "metamask.assets.set_custom_gas",
48
+ "metamask.assets.select_network_scope",
49
+ "metamask.wallet.lock",
50
+ "metamask.networks.read_visible_state",
51
+ "metamask.networks.add_custom",
52
+ "metamask.networks.add_chainlist",
53
+ "metamask.networks.remove_custom",
54
+ "metamask.perps.read_visible_state",
55
+ "metamask.perps.compare_provider_market",
56
+ "metamask.perps.read_eligibility",
57
+ "metamask.perps.select_market_filter",
58
+ "metamask.perps.close_visible_position",
59
+ "metamask.perps.set_market_favorite",
60
+ "metamask.perps.select_activity_filter",
61
+ "metamask.perps.open_balance_action",
62
+ "metamask.perps.open_position_action",
63
+ "metamask.perps.edit_margin",
64
+ "metamask.perps.update_position_tpsl",
65
+ "metamask.perps.read_funds_confirmation",
66
+ "metamask.swap_bridge.read_visible_state",
67
+ "metamask.swap_bridge.select_assets",
68
+ "metamask.swap_bridge.set_amount",
69
+ "metamask.swap_bridge.set_max_amount",
70
+ "metamask.swap_bridge.set_slippage",
71
+ "metamask.swap_bridge.submit_transaction",
72
+ "metamask.settings.set_basic_functionality",
73
+ "metamask.perps.read_snapshot",
33
74
  "metamask.perps.read_positions",
34
75
  "metamask.perps.ensure_positions",
35
76
  "metamask.perps.assert_positions",
@@ -47,8 +88,7 @@ const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
47
88
  "metamask.perps.capture_performance"
48
89
  ]);
49
90
  const CORE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
50
- "metamask.perps.read_account",
51
- "metamask.perps.read_snapshot"
91
+ "metamask.perps.read_account"
52
92
  ]);
53
93
  const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
54
94
  "metamask.wallet.setup",
@@ -61,7 +101,7 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
61
101
  ]);
62
102
  const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set(["ui.navigate"]);
63
103
  function requiresLiveAdapter(platform, action) {
64
- return LIVE_ONLY_PERPS_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
104
+ return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
65
105
  }
66
106
  function liveRuntimeConfigured(platform, node) {
67
107
  if (platform === "mobile" || platform === "core") return true;
@@ -209,6 +249,36 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
209
249
  ];
210
250
  const bundledActions = [
211
251
  ...walletActions,
252
+ "metamask.assets.read_visible_state",
253
+ ...platform === "extension" ? ["metamask.assets.read_details"] : [],
254
+ ...platform === "extension" ? ["metamask.assets.verify_sorting"] : [],
255
+ ...platform === "extension" ? ["metamask.assets.set_token_visibility"] : [],
256
+ ...platform === "extension" ? ["metamask.assets.import_custom_token"] : [],
257
+ ...platform === "extension" ? [
258
+ "metamask.assets.prepare_send",
259
+ "metamask.assets.review_send",
260
+ "metamask.assets.finish_send",
261
+ "metamask.assets.set_custom_gas",
262
+ "metamask.assets.select_network_scope"
263
+ ] : [],
264
+ ...platform === "extension" ? ["metamask.wallet.lock"] : [],
265
+ ...platform === "extension" ? ["metamask.networks.read_visible_state"] : [],
266
+ ...platform === "extension" ? ["metamask.networks.add_custom", "metamask.networks.add_chainlist", "metamask.networks.remove_custom"] : [],
267
+ "metamask.perps.read_visible_state",
268
+ ...platform === "extension" ? ["metamask.perps.compare_provider_market"] : [],
269
+ ...platform === "extension" ? ["metamask.perps.read_eligibility"] : [],
270
+ ...platform === "extension" ? [
271
+ "metamask.perps.open_position_action",
272
+ "metamask.perps.edit_margin",
273
+ "metamask.perps.update_position_tpsl"
274
+ ] : [],
275
+ "metamask.swap_bridge.read_visible_state",
276
+ "metamask.swap_bridge.select_assets",
277
+ "metamask.swap_bridge.set_amount",
278
+ ...platform === "extension" ? ["metamask.swap_bridge.set_max_amount"] : [],
279
+ ...platform === "extension" ? ["metamask.swap_bridge.set_slippage"] : [],
280
+ ...platform === "extension" ? ["metamask.swap_bridge.submit_transaction"] : [],
281
+ ...platform === "extension" ? ["metamask.settings.set_basic_functionality"] : [],
212
282
  "metamask.perps.read_positions",
213
283
  "metamask.perps.ensure_positions",
214
284
  "metamask.perps.assert_positions",
@@ -221,13 +291,14 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
221
291
  "metamask.perps.start_state",
222
292
  "metamask.perps.teardown_state",
223
293
  "metamask.perps.capture_performance",
294
+ ...platform === "extension" || platform === "core" ? ["metamask.perps.read_snapshot"] : [],
224
295
  ...platform === "mobile" ? [
225
296
  "metamask.perps.clear_performance_caches",
226
297
  "metamask.perps.measure_homepage_visible",
227
298
  "metamask.perps.prepare_local_snapshot_endpoint"
228
299
  ] : [],
229
300
  // read_account is core-only: only the headless core adapter implements it.
230
- ...platform === "core" ? ["metamask.perps.read_account", "metamask.perps.read_snapshot"] : []
301
+ ...platform === "core" ? ["metamask.perps.read_account"] : []
231
302
  ];
232
303
  const bundled = new Set(bundledActions);
233
304
  const actions = [.../* @__PURE__ */ new Set([...bundledActions, ...declaredCustomActions])];
@@ -662,6 +733,7 @@ async function handleMobileHud(payload, context) {
662
733
  }
663
734
  }
664
735
  async function hideMobileHudOnTeardown(projectRoot, env = {}) {
736
+ if (env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") return;
665
737
  if (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false") return;
666
738
  const input = {
667
739
  node: { bridge_timeout_ms: 8e3, cdp_timeout_ms: 5e3 },
@@ -803,7 +875,7 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
803
875
  if (live) return live.output;
804
876
  throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
805
877
  }
806
- if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action)) {
878
+ if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action) && (context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || ["ui.swipe", "ui.pan", "ui.drag", "ui.long_press", "ui.capture_surface"].includes(action))) {
807
879
  return executeNativeProviderAction(action, node, context, harness.createNativeUiTransport);
808
880
  }
809
881
  return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
@@ -1,6 +1,7 @@
1
1
  function resolveMetaMaskMobileLifecycleTarget(node, rawContext) {
2
2
  const context = rawContext;
3
3
  const platform = resolveMobilePlatform(node, context);
4
+ const opaqueRuntime = context.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
4
5
  const port = scalar(node.watcher_port ?? node.metro_port, "app.lifecycle.metro_port") ?? context.env.WATCHER_PORT ?? context.env.METRO_PORT ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
5
6
  const launchUrl = scalar(node.launch_url ?? node.launchUrl ?? node.url, "app.lifecycle.launch_url") ?? expoDevClientUrl(port, platform);
6
7
  if (platform === "android") {
@@ -8,9 +9,9 @@ function resolveMetaMaskMobileLifecycleTarget(node, rawContext) {
8
9
  platform,
9
10
  deviceId: scalar(node.adb_serial ?? node.android_device ?? node.device, "app.lifecycle.device") ?? context.env.ADB_SERIAL ?? context.env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL,
10
11
  appId: scalar(node.package_id ?? node.packageName ?? node.app_id, "app.lifecycle.package_id") ?? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask",
11
- launchUrl,
12
- metroPort: port,
13
- prelaunchCalls: expoBundlePrewarmCalls(port, "android")
12
+ launchUrl: opaqueRuntime ? void 0 : launchUrl,
13
+ metroPort: opaqueRuntime ? void 0 : port,
14
+ prelaunchCalls: opaqueRuntime ? [] : expoBundlePrewarmCalls(port, "android")
14
15
  };
15
16
  }
16
17
  return {
@@ -239,6 +239,22 @@ const PUBLIC_COMMAND_CONTRACTS = {
239
239
  }),
240
240
  positionals: [{ label: "platform", choices: ["ios", "android"] }]
241
241
  },
242
+ "runtime-launch": {
243
+ options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
244
+ "--cdp-port": value(),
245
+ "--chrome-user-data-dir": value(),
246
+ "--artifacts-dir": value(),
247
+ "--start-watch": bool(),
248
+ "--build-lavamoat": bool(),
249
+ "--remote-flag": value(),
250
+ "--artifact-file": value(),
251
+ "--artifact-version": value(),
252
+ "--artifact-build-id": value(),
253
+ "--artifact-package-id": value(),
254
+ "--artifact-sha256": value(),
255
+ "--artifact-cache": value()
256
+ })
257
+ },
242
258
  logs: {
243
259
  options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
244
260
  "--full": bool(),
@@ -40,6 +40,7 @@ import {
40
40
  synthesizeOneNodeRecipe,
41
41
  validateRecipeAdapterAware
42
42
  } from "./run-engine.js";
43
+ import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
43
44
  import { acquireCheckoutLock } from "../checkout-lock.js";
44
45
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
45
46
  import { recipeTrustFailure } from "../recipe-security.js";
@@ -267,7 +268,7 @@ async function handleCall(argv) {
267
268
  }
268
269
  try {
269
270
  const prepared = await prepareHeal(adapter, target, options, json, {
270
- skipMobileSourceFreshness: adapter === "mobile" && resolvedAction === "app.lifecycle"
271
+ skipMobileSourceFreshness: adapter === "mobile" && (resolvedAction === "app.lifecycle" || readMobileReleaseArtifactState(target) !== null)
271
272
  });
272
273
  if (typeof prepared === "number") return prepared;
273
274
  const { state, heal } = prepared;
@@ -289,7 +290,10 @@ async function handleCall(argv) {
289
290
  target,
290
291
  heal,
291
292
  state,
292
- () => recoverRunInfra(adapter, target, json)
293
+ () => recoverRunInfra(adapter, target, json, {
294
+ cdpPort: optionString(options, "cdpPort"),
295
+ watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
296
+ })
293
297
  );
294
298
  if (violation !== null) {
295
299
  const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
@@ -21,7 +21,11 @@ import {
21
21
  extensionProductConfigBlock,
22
22
  extensionProductConfigFingerprint
23
23
  } from "../../adapters/extension/product-config.js";
24
- import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
24
+ import {
25
+ isExtensionDistStale,
26
+ releaseArtifactState,
27
+ runtimeDistCheck
28
+ } from "../../adapters/extension/runtime-decision.js";
25
29
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
26
30
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
27
31
  import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
@@ -52,10 +56,28 @@ async function installExtensionDeps(target) {
52
56
  return result;
53
57
  }
54
58
  async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
59
+ const requestedCdpPort = process.env.CDP_PORT;
60
+ const requestedRecipeCdpPort = process.env.RECIPE_CDP_PORT;
61
+ const restoreRequestedPorts = () => {
62
+ if (requestedCdpPort) process.env.CDP_PORT = requestedCdpPort;
63
+ if (requestedRecipeCdpPort) process.env.RECIPE_CDP_PORT = requestedRecipeCdpPort;
64
+ };
55
65
  if (process.env.CHROME_USER_DATA_DIR) {
56
66
  process.env.CHROME_USER_DATA_DIR = path.resolve(process.env.CHROME_USER_DATA_DIR);
57
67
  }
58
- const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target);
68
+ const artifact = releaseArtifactState(target);
69
+ const artifactSnapshot = runtimeDistCheck(target, artifact);
70
+ const artifactLaunchable = !wantWatch && tier !== "build" && artifact.status === "valid" && artifactSnapshot.status === "fresh";
71
+ const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
72
+ restoreRequestedPorts();
73
+ if (artifact.status !== "none" && !reusable && !artifactLaunchable) {
74
+ const detail = artifact.status === "invalid" ? `identity is invalid: ${artifact.reason ?? "unknown reason"}` : "runtime is not healthy and reusable";
75
+ return {
76
+ status: EXIT.infra,
77
+ output: `RELEASE_ARTIFACT_NOT_REUSABLE: The loaded release artifact ${detail}.
78
+ Next: rerun runtime-launch with the same artifact source.`
79
+ };
80
+ }
59
81
  if (!reusable) {
60
82
  const block = extensionProductConfigBlock(target);
61
83
  if (block) {
@@ -67,6 +89,7 @@ Next: ${block.userAction}`
67
89
  }
68
90
  }
69
91
  await ensureHarnessFresh(target, "extension");
92
+ restoreRequestedPorts();
70
93
  if (wantWatch) {
71
94
  const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
72
95
  const watchArgs = ["--target", target];
@@ -78,20 +101,35 @@ Next: ${block.userAction}`
78
101
  return extensionRebuild(target);
79
102
  }
80
103
  if (reusable) {
104
+ if (artifact.status === "valid") {
105
+ console.error(colorHumanMessage(
106
+ `\u2192 extension release reuse \u2014 verified artifact ${artifact.manifestVersion ?? artifact.expectedVersion ?? ""} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no snapshot mutation)`
107
+ ));
108
+ return { status: EXIT.ok, output: "Verified release artifact runtime is already healthy." };
109
+ }
81
110
  return extensionReattach(target, displayMode);
82
111
  }
112
+ if (artifactLaunchable) {
113
+ return extensionLaunchReleaseArtifact(target, artifact);
114
+ }
83
115
  return extensionLaunchDevelopment(target);
84
116
  }
85
- async function extensionRuntimeReusable(target) {
117
+ async function extensionRuntimeReusable(target, artifact = releaseArtifactState(target)) {
86
118
  const cdpPort = process.env.CDP_PORT;
87
119
  if (!cdpPort) return false;
88
- if (isExtensionDistStale(target)) return false;
89
- if (isExtensionWatcherLive(target) && !harnessWatcherMatchesProductConfig(target)) return false;
90
- if (!extensionCompiledScriptsMatchProductConfig(target, expectedRuntimeDist(target))) return false;
120
+ const runtimeSnapshot = runtimeDistCheck(target, artifact);
121
+ const releaseArtifact = runtimeSnapshot.source === "release-artifact";
122
+ if (releaseArtifact) {
123
+ if (runtimeSnapshot.status !== "fresh") return false;
124
+ } else {
125
+ if (isExtensionDistStale(target)) return false;
126
+ if (isExtensionWatcherLive(target) && !harnessWatcherMatchesProductConfig(target)) return false;
127
+ if (!extensionCompiledScriptsMatchProductConfig(target, expectedRuntimeDist(target))) return false;
128
+ }
91
129
  for (let attempt = 0; attempt < 3; attempt += 1) {
92
130
  const reachable = await cdpVersionReachable(cdpPort);
93
131
  const nonceOwned = reachable && await cdpOwnedByRuntimeNonce(cdpPort, target);
94
- const owned = nonceOwned || reachable && cdpOwnedByExpectedRuntime(cdpPort, target);
132
+ const owned = nonceOwned || reachable && cdpOwnedByExpectedRuntime(cdpPort, target, releaseArtifact ? artifact.chromeUserDataDir : void 0);
95
133
  const targetPresent = owned && await cdpHasExpectedExtensionTarget(cdpPort, target);
96
134
  const healthy = targetPresent && await cdpRuntimeHealthy(cdpPort, target);
97
135
  if (healthy) return true;
@@ -118,9 +156,9 @@ function commandHasExactFlagValue(command, flag, value) {
118
156
  }
119
157
  return false;
120
158
  }
121
- function cdpOwnedByExpectedRuntime(port, target) {
159
+ function cdpOwnedByExpectedRuntime(port, target, profileOverride) {
122
160
  const pids = cdpListenerPids(port);
123
- const profile = expectedChromeProfile(target);
161
+ const profile = profileOverride ?? expectedChromeProfile(target);
124
162
  const runtimeDist = expectedRuntimeDist(target);
125
163
  for (const pid of pids) {
126
164
  try {
@@ -344,6 +382,33 @@ async function extensionLaunchDevelopment(target) {
344
382
  ));
345
383
  return spawnScriptStreaming(liveScript, liveArgs, target);
346
384
  }
385
+ async function extensionLaunchReleaseArtifact(target, artifact) {
386
+ if (!artifact.sourceDir || !artifact.provenancePath || !artifact.chromeUserDataDir) {
387
+ return {
388
+ status: EXIT.infra,
389
+ output: "RELEASE_ARTIFACT_NOT_REUSABLE: The loaded release artifact identity is incomplete.\nNext: rerun runtime-launch with the same artifact source."
390
+ };
391
+ }
392
+ const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
393
+ const liveArgs = [
394
+ "--target",
395
+ target,
396
+ "--external-artifact",
397
+ "--dist-dir",
398
+ artifact.sourceDir,
399
+ "--artifact-provenance",
400
+ artifact.provenancePath,
401
+ "--preserve-profile",
402
+ "--chrome-user-data-dir",
403
+ artifact.chromeUserDataDir
404
+ ];
405
+ if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
406
+ if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
407
+ console.error(colorHumanMessage(
408
+ `\u2192 extension release relaunch \u2014 verified artifact ${artifact.manifestVersion ?? artifact.expectedVersion ?? ""} \xB7 CDP :${process.env.CDP_PORT ?? "default"}`
409
+ ));
410
+ return spawnScriptStreaming(liveScript, liveArgs, target);
411
+ }
347
412
  function harnessWatcherMatchesProductConfig(target) {
348
413
  const runtimeDir = path.join(target, recipeRuntimeDir());
349
414
  const harnessPidFile = path.join(runtimeDir, "recipe-harness-webpack.pid");
@@ -562,6 +562,9 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
562
562
  stream.phase("verify");
563
563
  const verifyArgs = ["verify", "--adapter", adapter, "--target", target];
564
564
  if (adapter === "mobile" && mobileTarget) verifyArgs.push("--platform", mobileTarget);
565
+ if (adapter === "extension" && process.env.CDP_PORT) {
566
+ verifyArgs.push("--cdp-port", process.env.CDP_PORT);
567
+ }
565
568
  if (machine) verifyArgs.push("--json");
566
569
  const exitCode = await handleHarness(verifyArgs);
567
570
  if (stream.enabled) {
@@ -4,8 +4,10 @@ import {
4
4
  recordMobileSourceBaseline
5
5
  } from "../../adapters/mobile/source-freshness.js";
6
6
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
7
+ import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
7
8
  import { EXIT } from "../shared.js";
8
9
  async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false) {
10
+ clearMobileReleaseArtifactState(target);
9
11
  await ensureHarnessFresh(target, "mobile");
10
12
  const platform = mobileTarget ?? "ios";
11
13
  const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
@@ -1,6 +1,7 @@
1
1
  import { detectAdapter } from "../harness.js";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
+ import { clearMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
4
5
  import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
5
6
  import {
6
7
  applyWatcherPortOption,
@@ -28,6 +29,7 @@ async function handleProvision({ positional, options, rawArgv }) {
28
29
  const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
29
30
  if (adapter === "mobile") {
30
31
  writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
32
+ if (!optionFlag(options, "resolveOnly")) clearMobileReleaseArtifactState(target);
31
33
  }
32
34
  const result = await surface.runwayProvision.run(target, {
33
35
  json,
@@ -13,6 +13,9 @@ import {
13
13
  import { resolveActionManifest, validateManifest } from "../manifest.js";
14
14
  import { getAdapterSurface } from "../adapters/surface.js";
15
15
  import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
16
+ import {
17
+ requireCurrentMobileReleaseArtifactState
18
+ } from "../adapters/mobile/release-artifact-state.js";
16
19
  import {
17
20
  importRecipeHarness,
18
21
  importRecipeProtocol,
@@ -137,10 +140,11 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
137
140
  const { createMetaMaskRunner } = await import("../runner.js");
138
141
  const recipeDocument = runtimeRecipeDocument(recipe, absoluteRecipePath);
139
142
  const suppressHudForLifecycleTiming = adapter === "mobile" && mobileRecipeRequiresUninterruptedLifecycleTiming(recipeDocument);
143
+ const suppressHudForOpaqueMobile = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
140
144
  const runner = await createMetaMaskRunner(adapter, manifest, {
141
145
  quietStdout: runtimeOptions.stdoutIsMachineContract === true,
142
146
  suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
143
- autoHud: suppressHudForLifecycleTiming ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
147
+ autoHud: suppressHudForLifecycleTiming || suppressHudForOpaqueMobile ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
144
148
  onActionEvent: runtimeOptions.onActionEvent,
145
149
  actionSources,
146
150
  // A direct engineer invocation is the explicit trust boundary for a
@@ -216,6 +220,10 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
216
220
  const previousWatcherPort = process.env.WATCHER_PORT;
217
221
  const previousMetroPort = process.env.METRO_PORT;
218
222
  const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
223
+ const previousMobileOpaqueRuntime = process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME;
224
+ const previousAndroidPackageId = process.env.ANDROID_PACKAGE_ID;
225
+ const previousAdbSerial = process.env.ADB_SERIAL;
226
+ const previousAndroidSerial = process.env.ANDROID_SERIAL;
219
227
  getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
220
228
  if (runtimeOptions.cdpPort) {
221
229
  process.env.CDP_PORT = runtimeOptions.cdpPort;
@@ -228,6 +236,21 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
228
236
  if (runtimeOptions.launchExistingDist) {
229
237
  process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
230
238
  }
239
+ if (adapter === "mobile") {
240
+ const state = requireCurrentMobileReleaseArtifactState(projectRoot);
241
+ if (state) {
242
+ const selected = process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL;
243
+ if (selected && selected !== state.deviceSerial) {
244
+ throw new Error(
245
+ `The Mobile release artifact is installed on ${state.deviceSerial}, not ${selected}. Next: rerun runtime-launch for ${selected}.`
246
+ );
247
+ }
248
+ process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME = "1";
249
+ process.env.ANDROID_PACKAGE_ID = state.packageId;
250
+ process.env.ADB_SERIAL = state.deviceSerial;
251
+ process.env.ANDROID_SERIAL = state.deviceSerial;
252
+ }
253
+ }
231
254
  return () => {
232
255
  restoreEnv("CDP_PORT", previousCdpPort);
233
256
  restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
@@ -237,6 +260,13 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
237
260
  "METAMASK_RECIPE_EXTENSION_AUTOLAUNCH",
238
261
  previousExtensionAutolaunch
239
262
  );
263
+ restoreEnv(
264
+ "METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME",
265
+ previousMobileOpaqueRuntime
266
+ );
267
+ restoreEnv("ANDROID_PACKAGE_ID", previousAndroidPackageId);
268
+ restoreEnv("ADB_SERIAL", previousAdbSerial);
269
+ restoreEnv("ANDROID_SERIAL", previousAndroidSerial);
240
270
  };
241
271
  }
242
272
  function actionSourceMap(sources) {
@@ -267,12 +297,23 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
267
297
  ANDROID_DEVICE: process.env.ANDROID_DEVICE,
268
298
  ANDROID_TARGET_DEVICE_NAME: process.env.ANDROID_TARGET_DEVICE_NAME,
269
299
  ADB_SERIAL: process.env.ADB_SERIAL,
270
- ANDROID_SERIAL: process.env.ANDROID_SERIAL
300
+ ANDROID_SERIAL: process.env.ANDROID_SERIAL,
301
+ ANDROID_PACKAGE_ID: process.env.ANDROID_PACKAGE_ID,
302
+ METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME: process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME
271
303
  };
272
304
  }
273
305
  async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
274
306
  if (adapter !== "extension" || runtimeOptions.skipExtensionRuntimePrepare === true)
275
307
  return;
308
+ const { releaseArtifactState, runtimeDistCheck } = await import("../adapters/extension/runtime-decision.js");
309
+ const artifact = releaseArtifactState(projectRoot);
310
+ const runtimeSnapshot = runtimeDistCheck(projectRoot, artifact);
311
+ if (artifact.status !== "none") {
312
+ if (artifact.status === "valid" && runtimeSnapshot.status === "fresh") return;
313
+ throw new Error(
314
+ "The loaded release artifact identity is invalid or stale. Next: rerun runtime-launch with the same artifact source."
315
+ );
316
+ }
276
317
  const { prepareExtensionRuntime } = await import("../adapters/extension/runtime.js");
277
318
  await prepareExtensionRuntime({
278
319
  projectRoot,
@@ -306,7 +347,7 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
306
347
  );
307
348
  const findings = [
308
349
  ...withManifest.findings,
309
- ...validateMetaMaskActionInputs(recipe)
350
+ ...validateMetaMaskActionInputs(recipe, adapter)
310
351
  ];
311
352
  if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
312
353
  try {
@@ -336,7 +377,7 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
336
377
  manifest,
337
378
  validationOptions
338
379
  ).findings,
339
- ...validateMetaMaskActionInputs(dependency.document)
380
+ ...validateMetaMaskActionInputs(dependency.document, adapter)
340
381
  );
341
382
  }
342
383
  } catch (error) {
@@ -782,6 +823,7 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
782
823
  return EXIT.usage;
783
824
  }
784
825
  const state = newHealState();
826
+ const mobileArtifact = adapter === "mobile" ? requireCurrentMobileReleaseArtifactState(target) : null;
785
827
  prepareOptions.onPhase?.("install");
786
828
  const ensured = await ensureOverlay(
787
829
  adapter,
@@ -847,7 +889,7 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
847
889
  restoreEnv("WATCHER_PORT", previousWatcherPort);
848
890
  }
849
891
  }
850
- if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
892
+ if (adapter === "mobile" && mobileArtifact === null && prepareOptions.skipMobileSourceFreshness !== true) {
851
893
  const current = await ensureMobileProofRuntime(
852
894
  target,
853
895
  heal,
@@ -994,6 +1036,22 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
994
1036
  });
995
1037
  const initial = await decide();
996
1038
  if (initial.decision === "ready") return null;
1039
+ if (initial.reasonCode === "release-artifact-identity-invalid" || initial.reasonCode === "release-artifact-runtime-missing" || initial.reasonCode === "release-artifact-runtime-stale") {
1040
+ const message = initial.reasons[0] ?? "The loaded release artifact identity is invalid.";
1041
+ const userAction2 = "rerun runtime-launch with the same artifact source";
1042
+ if (json) {
1043
+ console.log(JSON.stringify({
1044
+ schemaVersion: 1,
1045
+ status: "fail",
1046
+ recoverable: false,
1047
+ error: { code: "RELEASE_ARTIFACT_NOT_CURRENT", message, userAction: userAction2 }
1048
+ }, null, 2));
1049
+ } else {
1050
+ console.error(`\u2717 ${message}`);
1051
+ console.error(` Next: ${userAction2}`);
1052
+ }
1053
+ return EXIT.infra;
1054
+ }
997
1055
  if (initial.reasonCode === "runtime-dist-check-failed") {
998
1056
  const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
999
1057
  const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
@@ -1139,18 +1197,54 @@ const RUN_RECOVERY_CODE = {
1139
1197
  extension: "extension.runtime_recovered",
1140
1198
  core: "run.retried"
1141
1199
  };
1142
- async function recoverRunInfra(adapter, target, json) {
1200
+ async function recoverRunInfra(adapter, target, json, runtimeOptions = {}) {
1143
1201
  getAdapterSurface(adapter).resolveSlotPorts(target);
1144
- if (adapter === "extension") {
1145
- const { launchExtension } = await import("./launch/extension.js");
1146
- return launchExtension(target, "quick", false);
1202
+ const previousCdpPort = process.env.CDP_PORT;
1203
+ const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
1204
+ if (runtimeOptions.cdpPort) {
1205
+ process.env.CDP_PORT = runtimeOptions.cdpPort;
1206
+ process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
1147
1207
  }
1148
- if (adapter === "mobile") {
1149
- const { launchMobile } = await import("./launch/mobile.js");
1150
- const platform = resolveMobileRecoveryPlatform();
1151
- return launchMobile(target, platform, "quick", json);
1208
+ try {
1209
+ if (adapter === "extension") {
1210
+ const { releaseArtifactState } = await import("../adapters/extension/runtime-decision.js");
1211
+ const artifact = releaseArtifactState(target);
1212
+ if (artifact.status !== "none" && artifact.status !== "valid") {
1213
+ return {
1214
+ status: 1,
1215
+ output: "Release artifact recovery cannot trust the recorded runtime identity.\nNext: rerun runtime-launch with the same artifact source."
1216
+ };
1217
+ }
1218
+ const { launchExtension } = await import("./launch/extension.js");
1219
+ return launchExtension(target, "quick", false);
1220
+ }
1221
+ if (adapter === "mobile") {
1222
+ try {
1223
+ const artifact = requireCurrentMobileReleaseArtifactState(target);
1224
+ if (artifact) {
1225
+ const { relaunchAndroidReleaseArtifactRuntime } = await import("../adapters/mobile/release-artifact.js");
1226
+ await relaunchAndroidReleaseArtifactRuntime(artifact);
1227
+ return {
1228
+ status: 0,
1229
+ output: `Relaunched ${artifact.packageId} ${artifact.versionName} (${artifact.versionCode}) on ${artifact.deviceSerial}.`
1230
+ };
1231
+ }
1232
+ } catch (error) {
1233
+ return {
1234
+ status: 1,
1235
+ output: `Release artifact recovery could not revalidate and relaunch the pinned Android package: ${error instanceof Error ? error.message : String(error)}
1236
+ Next: rerun runtime-launch with the same artifact source.`
1237
+ };
1238
+ }
1239
+ const { launchMobile } = await import("./launch/mobile.js");
1240
+ const platform = resolveMobileRecoveryPlatform();
1241
+ return launchMobile(target, platform, "quick", json);
1242
+ }
1243
+ return { status: 0, output: "headless run retry" };
1244
+ } finally {
1245
+ restoreEnv("CDP_PORT", previousCdpPort);
1246
+ restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
1152
1247
  }
1153
- return { status: 0, output: "headless run retry" };
1154
1248
  }
1155
1249
  function resolveMobileRecoveryPlatform(env = process.env) {
1156
1250
  if (env.PLATFORM === "android" || env.PLATFORM === "ios") return env.PLATFORM;
@@ -1251,6 +1345,7 @@ export {
1251
1345
  mobileRecipeRequiresUninterruptedLifecycleTiming,
1252
1346
  preflightRecipe,
1253
1347
  prepareHeal,
1348
+ prepareRuntimeIfNeeded,
1254
1349
  recoverRunInfra,
1255
1350
  resolveMetaMaskLibrarySources,
1256
1351
  resolveMobileRecoveryPlatform,
@@ -29,6 +29,7 @@ import { handleDescribeRecipe, handleListExecutables } from "./list-executables.
29
29
  import { applyDeviceTargeting } from "./device-target.js";
30
30
  import { getAdapterSurface } from "../adapters/surface.js";
31
31
  import { coreDependencyBlock } from "./core-readiness.js";
32
+ import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
32
33
  import { writeRunReport } from "./run-report.js";
33
34
  import { acquireCheckoutLock } from "../checkout-lock.js";
34
35
  import { JsonStreamWriter } from "../json-stream.js";
@@ -250,7 +251,8 @@ async function handleRunInner({ positional, options }, stream) {
250
251
  }
251
252
  try {
252
253
  const prepared = await prepareHeal(adapter, target, options, machine, {
253
- onPhase: (phase, fields) => stream.phase(phase, fields)
254
+ onPhase: (phase, fields) => stream.phase(phase, fields),
255
+ skipMobileSourceFreshness: adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
254
256
  });
255
257
  if (typeof prepared === "number") {
256
258
  stream.error({
@@ -282,7 +284,7 @@ async function handleRunInner({ positional, options }, stream) {
282
284
  target,
283
285
  heal,
284
286
  state,
285
- () => recoverRunInfra(adapter, target, machine),
287
+ () => recoverRunInfra(adapter, target, machine, runtimeOptions),
286
288
  (code) => stream.phase("recover", { code })
287
289
  );
288
290
  for (const mutation of state.mutations) stream.mutation(mutation);