@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
@@ -1,6 +1,14 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import {
5
+ acquireExtensionReleaseArtifact
6
+ } from "../adapters/extension/release-artifact.js";
7
+ import { installAndroidReleaseArtifact } from "../adapters/mobile/release-artifact.js";
8
+ import {
9
+ clearMobileReleaseArtifactState,
10
+ writeMobileReleaseArtifactState
11
+ } from "../adapters/mobile/release-artifact-state.js";
4
12
  import { recipeHarnessPath } from "../paths.js";
5
13
  import {
6
14
  adapterOption,
@@ -13,9 +21,56 @@ import {
13
21
  usageError
14
22
  } from "./parse-args.js";
15
23
  async function handleRuntimeLaunch({ options }) {
24
+ if (optionFlag(options, "help")) {
25
+ console.log(`mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
26
+
27
+ --artifact-file <path> Artifact downloaded by the validation skill
28
+ --artifact-version <X.Y.Z> Expected manifest or Android version name
29
+ --artifact-sha256 <digest> Trusted SHA-256 from the acquisition step
30
+ --artifact-build-id <id> Android version code
31
+ --artifact-package-id <id> Android application ID
32
+ --artifact-cache <dir> Artifact cache root
33
+ --platform <android> Mobile artifact platform (iOS is unsupported)
34
+ --device <serial> Explicit Android device serial
35
+ --cdp-port <port> Extension CDP port
36
+ --chrome-user-data-dir Isolated Chrome profile path
37
+ --artifacts-dir <dir> Validation evidence directory
38
+ --json Machine-readable terminal report
39
+
40
+ The harness verifies and runs a local artifact but never downloads one. Extension
41
+ launch resets an isolated profile. Android install requires an explicit serial.`);
42
+ return 0;
43
+ }
16
44
  const adapter = adapterOption(options);
17
45
  const target = targetPath(options);
46
+ const artifactFile = optionString(options, "artifactFile");
47
+ const artifactVersion = optionString(options, "artifactVersion");
48
+ const artifactSha256 = optionString(options, "artifactSha256");
49
+ const artifactBuildId = optionString(options, "artifactBuildId");
50
+ const artifactPackageId = optionString(options, "artifactPackageId");
51
+ const artifactCache = optionString(options, "artifactCache");
52
+ const artifactRequested = Boolean(
53
+ artifactFile || artifactVersion || artifactSha256 || artifactBuildId || artifactPackageId || artifactCache
54
+ );
55
+ if (artifactRequested && (!artifactFile || !artifactVersion || !artifactSha256)) {
56
+ throw usageError("artifact launch requires --artifact-file, --artifact-version, and --artifact-sha256 together.");
57
+ }
58
+ if (adapter === "mobile") {
59
+ return handleAndroidArtifactLaunch({
60
+ options,
61
+ target,
62
+ artifactFile,
63
+ artifactVersion,
64
+ artifactSha256,
65
+ artifactBuildId,
66
+ artifactPackageId,
67
+ artifactCache
68
+ });
69
+ }
18
70
  if (adapter !== "extension") throw new Error("runtime-launch currently applies to the extension adapter.");
71
+ if (artifactBuildId || artifactPackageId || optionString(options, "platform") || optionString(options, "device")) {
72
+ throw usageError("Extension runtime-launch does not accept Android artifact options.");
73
+ }
19
74
  const cdpPort = parsePort(
20
75
  optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
21
76
  "runtime-launch requires --cdp-port <port>."
@@ -29,7 +84,19 @@ async function handleRuntimeLaunch({ options }) {
29
84
  if (startWatch && buildLavaMoat) {
30
85
  throw usageError("runtime-launch accepts only one of --start-watch or --build-lavamoat.");
31
86
  }
32
- const mode = buildLavaMoat ? "build-lavamoat" : startWatch ? "start-watch" : "existing-dist";
87
+ if (artifactRequested && (startWatch || buildLavaMoat || optionString(options, "remoteFlag"))) {
88
+ throw usageError("artifact launch cannot be combined with build, watch, or remote-flag options.");
89
+ }
90
+ let artifact;
91
+ if (artifactRequested) {
92
+ artifact = await acquireExtensionReleaseArtifact({
93
+ file: artifactFile,
94
+ expectedVersion: artifactVersion,
95
+ expectedSha256: artifactSha256,
96
+ cacheRoot: artifactCache
97
+ });
98
+ }
99
+ const mode = artifact ? "release-artifact" : buildLavaMoat ? "build-lavamoat" : startWatch ? "start-watch" : "existing-dist";
33
100
  const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
34
101
  const command = [
35
102
  "bash",
@@ -42,6 +109,15 @@ async function handleRuntimeLaunch({ options }) {
42
109
  "--artifacts-dir",
43
110
  artifactsDir
44
111
  ];
112
+ if (artifact) {
113
+ command.push(
114
+ "--external-artifact",
115
+ "--dist-dir",
116
+ artifact.extensionDir,
117
+ "--artifact-provenance",
118
+ artifact.provenancePath
119
+ );
120
+ }
45
121
  if (chromeUserDataDir) command.push("--chrome-user-data-dir", chromeUserDataDir);
46
122
  const remoteFlag = optionString(options, "remoteFlag");
47
123
  if (remoteFlag) command.push("--remote-flag", remoteFlag);
@@ -52,7 +128,7 @@ async function handleRuntimeLaunch({ options }) {
52
128
  cdpPort,
53
129
  artifactsDir,
54
130
  reason: "harness_live_script_missing",
55
- fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode)}`,
131
+ fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode, artifactInvocation(options))}`,
56
132
  command
57
133
  });
58
134
  printRuntimeLaunchReport(report2, optionFlag(options, "json"));
@@ -61,7 +137,7 @@ async function handleRuntimeLaunch({ options }) {
61
137
  fs.mkdirSync(artifactsDir, { recursive: true });
62
138
  const jsonMode = optionFlag(options, "json");
63
139
  if (!jsonMode) {
64
- const detail = mode === "build-lavamoat" ? "production-like LavaMoat build" : mode === "start-watch" ? "test webpack watcher" : "existing dist";
140
+ const detail = mode === "release-artifact" ? `release artifact ${artifact?.manifestVersion}` : mode === "build-lavamoat" ? "production-like LavaMoat build" : mode === "start-watch" ? "test webpack watcher" : "existing dist";
65
141
  console.error(`recipe: runtime-launch starting (cdp=${cdpPort}, ${detail})`);
66
142
  } else {
67
143
  console.error(JSON.stringify({
@@ -69,7 +145,7 @@ async function handleRuntimeLaunch({ options }) {
69
145
  type: "progress",
70
146
  command: "rebuild",
71
147
  phase: "runtime-launch",
72
- message: mode === "build-lavamoat" ? "production-like LavaMoat build starting" : mode === "start-watch" ? "test webpack watcher starting" : "launching existing dist",
148
+ message: mode === "release-artifact" ? `release artifact ${artifact?.manifestVersion} launch starting` : mode === "build-lavamoat" ? "production-like LavaMoat build starting" : mode === "start-watch" ? "test webpack watcher starting" : "launching existing dist",
73
149
  cdpPort
74
150
  }));
75
151
  }
@@ -90,7 +166,8 @@ async function handleRuntimeLaunch({ options }) {
90
166
  summaryPath,
91
167
  reason: "runtime_ready",
92
168
  fix: "",
93
- command
169
+ command,
170
+ artifact
94
171
  });
95
172
  printRuntimeLaunchReport(report2, optionFlag(options, "json"));
96
173
  return 0;
@@ -109,20 +186,111 @@ async function handleRuntimeLaunch({ options }) {
109
186
  launchLogPath: fs.existsSync(launchLogPath) ? launchLogPath : void 0,
110
187
  verifySummaryPath: fs.existsSync(verifySummaryPath) ? verifySummaryPath : void 0,
111
188
  reason,
112
- fix: `Read ${failurePath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode)}`,
189
+ fix: `Read ${failurePath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode, artifactInvocation(options))}`,
113
190
  command,
191
+ artifact,
114
192
  exitCode: result.status ?? 1
115
193
  });
116
194
  printRuntimeLaunchReport(report, optionFlag(options, "json"));
117
195
  return 1;
118
196
  }
197
+ async function handleAndroidArtifactLaunch(input) {
198
+ const {
199
+ options,
200
+ target,
201
+ artifactFile,
202
+ artifactVersion,
203
+ artifactSha256,
204
+ artifactBuildId,
205
+ artifactPackageId,
206
+ artifactCache
207
+ } = input;
208
+ const platform = optionString(options, "platform") ?? "android";
209
+ if (platform === "ios") {
210
+ throw usageError("Downloaded iOS artifact launch is unsupported. Next: use --platform android with a local APK.");
211
+ }
212
+ if (platform !== "android") throw usageError("--platform must be android for Mobile artifact launch.");
213
+ if (!artifactFile || !artifactVersion || !artifactSha256 || !artifactBuildId || !artifactPackageId) {
214
+ throw usageError(
215
+ "Android artifact launch requires --artifact-file, --artifact-version, --artifact-build-id, --artifact-package-id, and --artifact-sha256 together."
216
+ );
217
+ }
218
+ const deviceSerial = optionString(options, "device");
219
+ if (!deviceSerial) {
220
+ throw usageError("Android artifact launch requires --device <serial>. Next: run adb devices -l and rerun with one serial.");
221
+ }
222
+ if (artifactCache || optionString(options, "cdpPort") || optionString(options, "chromeUserDataDir") || optionString(options, "remoteFlag") || optionFlag(options, "startWatch") || optionFlag(options, "buildLavamoat")) {
223
+ throw usageError("Android artifact launch cannot be combined with Extension build, profile, cache, or CDP options.");
224
+ }
225
+ const artifactsDir = path.resolve(
226
+ optionString(options, "artifactsDir") ?? recipeHarnessPath(target, "mobile", "runtime-launch", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, ""))
227
+ );
228
+ fs.mkdirSync(artifactsDir, { recursive: true });
229
+ const summaryPath = path.join(artifactsDir, "summary.json");
230
+ try {
231
+ clearMobileReleaseArtifactState(target);
232
+ const artifact = await installAndroidReleaseArtifact({
233
+ file: artifactFile,
234
+ expectedVersion: artifactVersion,
235
+ expectedBuildId: artifactBuildId,
236
+ expectedSha256: artifactSha256,
237
+ packageId: artifactPackageId,
238
+ deviceSerial
239
+ });
240
+ const runtimeState = writeMobileReleaseArtifactState(target, artifact);
241
+ const report = runtimeLaunchReport("pass", {
242
+ adapter: "mobile",
243
+ platform,
244
+ target,
245
+ artifactsDir,
246
+ summaryPath,
247
+ reason: "runtime_ready",
248
+ fix: "",
249
+ artifact,
250
+ runtimeState
251
+ });
252
+ fs.writeFileSync(summaryPath, `${JSON.stringify(report, null, 2)}
253
+ `, { encoding: "utf8", mode: 384 });
254
+ printRuntimeLaunchReport(report, optionFlag(options, "json"));
255
+ return 0;
256
+ } catch (error) {
257
+ const message = error instanceof Error ? error.message : String(error);
258
+ const report = runtimeLaunchReport("fail", {
259
+ adapter: "mobile",
260
+ platform,
261
+ target,
262
+ artifactsDir,
263
+ summaryPath,
264
+ reason: "android_artifact_launch_failed",
265
+ error: message,
266
+ fix: `Fix the first error, then rerun with --device ${shellQuote(deviceSerial)} and the same verified artifact identity.`
267
+ });
268
+ fs.writeFileSync(summaryPath, `${JSON.stringify(report, null, 2)}
269
+ `, { encoding: "utf8", mode: 384 });
270
+ printRuntimeLaunchReport(report, optionFlag(options, "json"));
271
+ return 1;
272
+ }
273
+ }
119
274
  function runtimeLaunchLiveFlag(mode) {
120
- return mode === "existing-dist" ? "--launch-existing-dist" : `--${mode}`;
275
+ return mode === "existing-dist" || mode === "release-artifact" ? "--launch-existing-dist" : `--${mode}`;
121
276
  }
122
- function runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode = "existing-dist") {
277
+ function runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode = "existing-dist", artifact) {
123
278
  const base = `mm-harness runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
124
- const withMode = mode === "existing-dist" ? base : `${base} --${mode}`;
125
- return chromeUserDataDir ? `${withMode} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : withMode;
279
+ let command = mode === "existing-dist" || mode === "release-artifact" ? base : `${base} --${mode}`;
280
+ if (artifact?.file) command += ` --artifact-file ${shellQuote(artifact.file)}`;
281
+ if (artifact?.version) command += ` --artifact-version ${shellQuote(artifact.version)}`;
282
+ if (artifact?.sha256) command += ` --artifact-sha256 ${shellQuote(artifact.sha256)}`;
283
+ if (artifact?.cache) command += ` --artifact-cache ${shellQuote(artifact.cache)}`;
284
+ return chromeUserDataDir ? `${command} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : command;
285
+ }
286
+ function artifactInvocation(options) {
287
+ const artifact = {
288
+ file: optionString(options, "artifactFile"),
289
+ version: optionString(options, "artifactVersion"),
290
+ sha256: optionString(options, "artifactSha256"),
291
+ cache: optionString(options, "artifactCache")
292
+ };
293
+ return Object.values(artifact).some(Boolean) ? artifact : void 0;
126
294
  }
127
295
  function runtimeLaunchReport(status, fields) {
128
296
  return {
@@ -86,7 +86,7 @@ function checkHealBounds(target, output, state) {
86
86
  originalError
87
87
  };
88
88
  }
89
- const failureClass = classifyFailure(output);
89
+ const failureClass = classifyFailure(conciseFailureForHuman(output));
90
90
  if (failureClass === "screenshot-protected") {
91
91
  return {
92
92
  code: "SCREENSHOT_PROTECTED",
@@ -158,7 +158,9 @@ async function prepareLiveAdapterScript({
158
158
  target: `node${process.versions.node.split(".")[0]}`,
159
159
  legalComments: "none",
160
160
  sourcemap: false,
161
- banner: { js: `process.argv[1] = ${JSON.stringify(entryReal)};` },
161
+ banner: {
162
+ js: `import { createRequire as __mmCreateRequire } from 'node:module';const require = __mmCreateRequire(import.meta.url);process.argv[1] = ${JSON.stringify(entryReal)};`
163
+ },
162
164
  plugins: [bindImportMetaUrls()],
163
165
  logLevel: "silent"
164
166
  });
@@ -1,7 +1,7 @@
1
1
  function isRecord(value) {
2
2
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
3
  }
4
- function validateMetaMaskActionInputs(recipe) {
4
+ function validateMetaMaskActionInputs(recipe, adapter) {
5
5
  if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
6
6
  const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
7
7
  if (!nodes) return [];
@@ -9,6 +9,52 @@ function validateMetaMaskActionInputs(recipe) {
9
9
  for (const [nodeId, node] of Object.entries(nodes)) {
10
10
  if (!isRecord(node)) continue;
11
11
  const action = node.action;
12
+ if (action === "metamask.perps.read_snapshot" && adapter === "extension") {
13
+ if (node.market !== void 0 && node.markets !== void 0) {
14
+ findings.push({
15
+ severity: "error",
16
+ code: "recipe.invalid_param",
17
+ path: `workflow.nodes.${nodeId}.markets`,
18
+ message: `${action} accepts market or markets, not both`
19
+ });
20
+ }
21
+ if (isRecord(node.history)) {
22
+ const start = node.history.start_time_ms;
23
+ const end = node.history.end_time_ms;
24
+ if (typeof start === "number" && typeof end === "number" && start > end) {
25
+ findings.push({
26
+ severity: "error",
27
+ code: "recipe.invalid_param",
28
+ path: `workflow.nodes.${nodeId}.history.start_time_ms`,
29
+ message: `${action} history.start_time_ms must not exceed history.end_time_ms`
30
+ });
31
+ }
32
+ if (typeof node.history.limit === "number" && node.history.limit > 500) {
33
+ findings.push({
34
+ severity: "error",
35
+ code: "recipe.invalid_param",
36
+ path: `workflow.nodes.${nodeId}.history.limit`,
37
+ message: `${action} history.limit must not exceed 500`
38
+ });
39
+ }
40
+ }
41
+ if (typeof node.max_items_per_section === "number" && node.max_items_per_section > 500) {
42
+ findings.push({
43
+ severity: "error",
44
+ code: "recipe.invalid_param",
45
+ path: `workflow.nodes.${nodeId}.max_items_per_section`,
46
+ message: `${action} max_items_per_section must not exceed 500`
47
+ });
48
+ }
49
+ if (typeof node.timeout_ms === "number" && node.timeout_ms > 6e4) {
50
+ findings.push({
51
+ severity: "error",
52
+ code: "recipe.invalid_param",
53
+ path: `workflow.nodes.${nodeId}.timeout_ms`,
54
+ message: `${action} timeout_ms must not exceed 60000`
55
+ });
56
+ }
57
+ }
12
58
  const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
13
59
  const createsOrder = action === "metamask.perps.place_order" || (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
14
60
  if (!createsOrder) continue;
@@ -199,6 +199,7 @@ Example:
199
199
  --list List actions accepted by call for this adapter; no <action> needed
200
200
  --arg k=v Action field value (repeatable; equivalent to key=value shorthand)
201
201
  --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
202
+ --cdp-port <port> Extension only: bind the action to this live browser runtime.
202
203
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
203
204
  --target <path> Checkout path (default: cwd)
204
205
  --artifacts-dir <dir> Where to write evidence (default: temp dir)
@@ -537,6 +538,36 @@ Example:
537
538
  mm-harness update
538
539
  mm-harness update --check --json`
539
540
  },
541
+ {
542
+ name: "runtime-launch",
543
+ summary: "Launch and verify an Extension runtime or local Android release artifact.",
544
+ example: "mm-harness runtime-launch --adapter extension --target <checkout> --cdp-port 9222 --artifact-file <zip> --artifact-version <X.Y.Z> --artifact-sha256 <digest>",
545
+ helpText: `mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
546
+
547
+ Verify and run an artifact already downloaded by the validation skill. The
548
+ harness performs no network acquisition. Extension ZIPs run through the
549
+ packaged browser/CDP path; Android APKs install to one explicitly named device.
550
+
551
+ --artifact-file <path> Artifact downloaded by the validation skill
552
+ --artifact-version <X.Y.Z> Expected manifest or Android version name
553
+ --artifact-build-id <id> Android version code
554
+ --artifact-package-id <id> Android application ID
555
+ --artifact-sha256 <digest> Trusted SHA-256 from the acquisition step
556
+ --artifact-cache <dir> Extension artifact cache root
557
+ --platform <android> Mobile platform (downloaded iOS is unsupported)
558
+ --device <serial> Explicit Android device serial
559
+ --cdp-port <port> Extension CDP port
560
+ --chrome-user-data-dir <dir> Isolated Chrome profile path
561
+ --artifacts-dir <dir> Validation evidence directory
562
+ --start-watch Build through the checkout watcher
563
+ --build-lavamoat Build through the production-like path
564
+ --remote-flag <KEY=VARIANT> Pin checkout snapshot flags (not allowed for artifacts)
565
+ --json Machine-readable terminal report
566
+
567
+ Example:
568
+ mm-harness runtime-launch --adapter extension --target /path/to/checkout --cdp-port 9222 --artifact-file /path/to/metamask-chrome-X.Y.Z.zip --artifact-version X.Y.Z --artifact-sha256 <digest>
569
+ mm-harness runtime-launch --adapter mobile --platform android --target /path/to/checkout --device <serial> --artifact-file /path/to/app.apk --artifact-version X.Y.Z --artifact-build-id <code> --artifact-package-id io.metamask --artifact-sha256 <digest>`
570
+ },
540
571
  {
541
572
  name: "fixtures",
542
573
  summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / reset / generate / finalize.",
@@ -745,7 +776,6 @@ const HIDDEN = [
745
776
  "completion-candidates",
746
777
  "runtime-health",
747
778
  "runtime-decision",
748
- "runtime-launch",
749
779
  "resolve-extension",
750
780
  "ensure-ready",
751
781
  "self-test"
@@ -3,10 +3,19 @@ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
5
5
  "ui.locators",
6
+ "ui.activate_and_observe",
6
7
  "metamask.wallet.fixture_status",
7
8
  "metamask.wallet.validate_import",
8
9
  "metamask.wallet.list_accounts",
9
10
  "metamask.wallet.read_state",
11
+ "metamask.assets.read_visible_state",
12
+ "metamask.assets.read_details",
13
+ "metamask.networks.read_visible_state",
14
+ "metamask.perps.read_visible_state",
15
+ "metamask.perps.compare_provider_market",
16
+ "metamask.perps.read_eligibility",
17
+ "metamask.perps.read_funds_confirmation",
18
+ "metamask.swap_bridge.read_visible_state",
10
19
  "metamask.perps.read_positions",
11
20
  "metamask.perps.read_orders",
12
21
  "metamask.perps.read_account",
@@ -19,15 +28,37 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
19
28
  "metamask.analytics.assert_events"
20
29
  ]);
21
30
  const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
31
+ "ui.activate_and_observe",
32
+ "metamask.assets.verify_sorting",
33
+ "metamask.assets.set_token_visibility",
34
+ "metamask.assets.import_custom_token",
35
+ "metamask.assets.prepare_send",
36
+ "metamask.assets.review_send",
37
+ "metamask.assets.finish_send",
38
+ "metamask.assets.set_custom_gas",
39
+ "metamask.assets.select_network_scope",
40
+ "metamask.wallet.lock",
41
+ "metamask.networks.add_custom",
42
+ "metamask.networks.add_chainlist",
43
+ "metamask.networks.remove_custom",
22
44
  "metamask.wallet.setup",
23
45
  "metamask.wallet.import",
24
46
  "metamask.wallet.reset",
25
47
  "metamask.wallet.ensure_unlocked",
26
48
  "metamask.wallet.select_account",
49
+ "metamask.swap_bridge.select_assets",
50
+ "metamask.swap_bridge.set_amount",
51
+ "metamask.swap_bridge.set_max_amount",
52
+ "metamask.swap_bridge.set_slippage",
53
+ "metamask.swap_bridge.submit_transaction",
54
+ "metamask.settings.set_basic_functionality",
27
55
  "metamask.deeplink.open",
28
56
  "metamask.perps.close_positions",
29
57
  "metamask.perps.close_orders",
30
58
  "metamask.perps.place_order",
59
+ "metamask.perps.close_visible_position",
60
+ "metamask.perps.open_position_action",
61
+ "metamask.perps.edit_margin",
31
62
  "metamask.perps.edit_order",
32
63
  "metamask.perps.update_position_tpsl",
33
64
  "metamask.perps.ensure_positions",
@@ -35,15 +66,24 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
35
66
  "metamask.perps.clear_performance_caches",
36
67
  "metamask.perps.measure_homepage_visible",
37
68
  "metamask.perps.prepare_local_snapshot_endpoint",
69
+ "metamask.perps.set_market_favorite",
70
+ "metamask.perps.select_activity_filter",
71
+ "metamask.perps.select_market_filter",
72
+ "metamask.perps.open_balance_action",
38
73
  "metamask.perps.start_state",
39
74
  "metamask.perps.teardown_state",
40
75
  // Consent changes wallet state and is therefore an app mutation.
41
76
  "metamask.analytics.set_consent"
42
77
  ]);
43
78
  const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
79
+ "ui.activate_and_observe",
80
+ "metamask.assets.finish_send",
81
+ "metamask.swap_bridge.submit_transaction",
44
82
  "metamask.perps.close_positions",
45
83
  "metamask.perps.close_orders",
46
84
  "metamask.perps.place_order",
85
+ "metamask.perps.close_visible_position",
86
+ "metamask.perps.edit_margin",
47
87
  "metamask.perps.edit_order",
48
88
  "metamask.perps.update_position_tpsl",
49
89
  "metamask.perps.ensure_positions",
@@ -32,7 +32,7 @@ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
32
32
  async function beginRunDiagnostics(adapter, projectRoot) {
33
33
  const source = getAdapterSurface(adapter).appLogSource(projectRoot);
34
34
  if (!source) return null;
35
- const mobileIssueBuffer = adapter === "mobile" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
35
+ const mobileIssueBuffer = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== "1" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
36
36
  if (adapter === "extension") {
37
37
  await ensureExtensionConsoleCapture(projectRoot).catch(() => void 0);
38
38
  }
package/dist/runner.js CHANGED
@@ -246,6 +246,9 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
246
246
  return lifecycle.map((entry) => ({
247
247
  ...entry,
248
248
  async execute(node, context) {
249
+ if (context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") {
250
+ return entry.execute(node, context);
251
+ }
249
252
  const command = node.command ?? node.event ?? node.state;
250
253
  const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
251
254
  const checksIosContinuity = command === "foreground" && !isAndroidLifecycle(node, context.env ?? {});
@@ -0,0 +1,125 @@
1
+ # Release QA capability map
2
+
3
+ This document maps the current mm-harness release-validation surface to the
4
+ MetaMask Extension QA checklists at commit
5
+ [`77591c5`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/tree/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension).
6
+ It describes reusable capabilities, not a release verdict. A release verdict
7
+ must come from the case inventory, terminal traces, and completion gate for that
8
+ specific artifact.
9
+
10
+ The referenced plans contain 135 cases:
11
+
12
+ - Perps: [64 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/perps-rc/perps-rc-regression.md)
13
+ - Assets: [22 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/assets-rc/assets-rc-regression.md)
14
+ - Swap and Bridge: [39 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/swap-bridge-rc/swap-bridge-rc-regression.md)
15
+ - Networks: [10 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/networks-regression-rc/networks-regression-rc.md)
16
+
17
+ ## Artifact boundary
18
+
19
+ Artifact discovery and download belong to the release-validation workflow. The
20
+ workflow supplies mm-harness with a local artifact, expected product identity,
21
+ and trusted SHA-256. mm-harness never discovers or downloads a release.
22
+
23
+ For Extension, `runtime-launch` verifies the ZIP digest and manifest version,
24
+ extracts it safely, records archive and tree provenance, copies the exact tree
25
+ to an isolated runtime directory, resets only the selected Chrome profile, and
26
+ launches without building or patching the checkout.
27
+
28
+ For Android, `runtime-launch` requires one physical-device serial, verifies the
29
+ local APK digest, package ID, version name, and version code, installs it, and
30
+ binds subsequent recipes to the installed APK digest and selected device. The
31
+ release path does not use Metro, a development bridge, or the HUD.
32
+
33
+ ```sh
34
+ mm-harness runtime-launch \
35
+ --adapter extension \
36
+ --target /path/to/metamask-extension \
37
+ --cdp-port 9222 \
38
+ --artifact-file /path/to/metamask-chrome-X.Y.Z.zip \
39
+ --artifact-version X.Y.Z \
40
+ --artifact-sha256 <trusted-digest>
41
+
42
+ mm-harness runtime-launch \
43
+ --adapter mobile \
44
+ --platform android \
45
+ --target /path/to/metamask-mobile \
46
+ --device <physical-device-serial> \
47
+ --artifact-file /path/to/app.apk \
48
+ --artifact-version X.Y.Z \
49
+ --artifact-build-id <version-code> \
50
+ --artifact-package-id io.metamask \
51
+ --artifact-sha256 <trusted-digest>
52
+ ```
53
+
54
+ ## Completion gate
55
+
56
+ [`scripts/validate-release-report.mjs`](../scripts/validate-release-report.mjs)
57
+ compares a canonical case inventory with terminal verdicts. It rejects missing
58
+ or unknown IDs, duplicate IDs, non-terminal statuses, absent evidence, evidence
59
+ outside the declared root, incomplete run directories, and any non-empty
60
+ `remainingCases`. A passing gate writes `metadata.json`,
61
+ `report-quality.json`, and `report.md`.
62
+
63
+ Supported verdicts are `PASS`, `FAIL`, `BLOCKED_FUNDS`, `BLOCKED_COST`, and
64
+ `BLOCKED_ENVIRONMENT`. Product failures must remain failures; infrastructure,
65
+ funding, and cost limits must not be reported as passes.
66
+
67
+ ## Capability matrix
68
+
69
+ `AVAILABLE` means a typed action or bundled recipe covers the reusable
70
+ operation and produces machine-readable evidence. `PARTIAL` means the generic
71
+ operation exists but the complete checklist family still depends on task-local
72
+ composition, live funds, or deterministic external state. `MISSING` means the
73
+ current manifests do not expose the required durable capability.
74
+
75
+ ### Perps
76
+
77
+ | Case family | Required state and behavior | Current capability | Status |
78
+ | --- | --- | --- | --- |
79
+ | Navigation, home, markets, and details | Eligible account, live catalog, market summaries, chart, statistics, and route recovery | Visible-state reads, market navigation/search/filter, provider comparison, eligibility reads, and market-detail recipes | **AVAILABLE** for visible live flows |
80
+ | Watchlist and activity | Existing favorites and typed activity rows | Favorite mutation, activity-filter selection, and release recipes | **AVAILABLE** |
81
+ | Order entry | Funded Perps balance, market/limit inputs, validation, preview, fill, and duplicate protection | Parameterized order placement plus visible order-entry and validation recipes | **PARTIAL**: live state and provider outcomes remain release-dependent |
82
+ | Position management | Open position, close, margin, and TP/SL changes | Visible position actions, close, edit-margin, and TP/SL actions | **PARTIAL**: exact outcome depends on live account state |
83
+ | Deposit and withdrawal | Wallet funds, Perps balance, confirmations, and resulting activity | Balance actions and confirmation reads with a funds-flow recipe | **PARTIAL**: funds and provider availability are external |
84
+ | Snapshot assertions | Positions, orders, markets, history, and watchlist | Bounded redacted snapshot when the background hook exists; visible-state reads on untouched releases | **AVAILABLE** with runtime-specific observation paths |
85
+ | Deterministic seeded provider state | Balance, positions, limit orders, TP/SL, history, and controlled REST/stream behavior | Existing setup fixtures do not reproduce the complete QA provider model inside an untouched release artifact | **MISSING** |
86
+
87
+ ### Assets
88
+
89
+ | Case family | Required state and behavior | Current capability | Status |
90
+ | --- | --- | --- | --- |
91
+ | Token list, prices, details, and sorting | Funded account with known holdings and price data | Visible token-list/details reads, network scope, sorting assertions, and bundled recipes | **AVAILABLE** for EVM token UI |
92
+ | Token search/import/hide/persistence | Known contracts and a persistent profile | Custom-token import, visibility changes, restart-safe recipes, and custom-network composition | **AVAILABLE** for Extension EVM tokens |
93
+ | Native/token send and gas | Funded owned accounts and live gas | Prepare, review, custom-gas, finish, and Max/cancel/send recipes | **PARTIAL**: terminal submission is gated by funds and cost policy |
94
+ | Aggregate Android holdings | Imported wallet with multi-chain assets | Native-accessibility token-list read on an untouched physical-device release | **AVAILABLE** |
95
+ | Balance streaming and aggregation arithmetic | Triggered inbound update and expected cross-network totals | Visible snapshots exist; deterministic inbound trigger and tolerance assertions are not bundled | **PARTIAL** |
96
+ | NFTs, DeFi, Solana send, and Bitcoin details | Indexed owned positions/assets and chain-specific transaction flows | Generic visible UI only | **MISSING** |
97
+
98
+ ### Swap and Bridge
99
+
100
+ | Case family | Required state and behavior | Current capability | Status |
101
+ | --- | --- | --- | --- |
102
+ | Surface, asset, network, and amount selection | Supported source/destination pair | Exact asset/network selection, amount/Max input, and visible-state reads on Extension; exact native selection and amount input on Android | **AVAILABLE** |
103
+ | Quote and slippage | Live quote services and supported route | Surface, quote, Max-quote, and custom-slippage recipes | **AVAILABLE** |
104
+ | Submit and confirmation | Sufficient source balance and gas | Typed submit action with explicit confirmation policy | **PARTIAL**: live funds and cost policy decide whether submission is allowed |
105
+ | Sequential multi-network journey | Balances delivered by preceding swaps and bridges | Individual steps are composable, but the complete live-funds journey remains task-local and resumable by its case inventory | **PARTIAL** |
106
+ | Activity, balance deltas, and completed bridge tracking | Confirmed transactions and indexed activity | Visible-state evidence exists; reusable terminal activity and arithmetic assertions are incomplete | **PARTIAL** |
107
+
108
+ ### Networks
109
+
110
+ | Case family | Required state and behavior | Current capability | Status |
111
+ | --- | --- | --- | --- |
112
+ | Add, select, and remove custom networks | Known RPC configuration and chain identity | Typed custom-network and Chainlist add, visible-state read, select, and remove actions | **AVAILABLE** on Extension |
113
+ | Custom-network tokens and persistence | Known token contract and persistent profile | Network actions compose with Assets import/visibility/persistence recipes | **AVAILABLE** on Extension |
114
+ | Balance stability and fiat visibility | Funded custom network and live price APIs | Visible-state reads and a bounded balance-stability recipe | **PARTIAL**: external price availability remains uncontrolled |
115
+ | Send Max and activity | Funded custom network and recipient | Assets send actions cover the transaction path | **PARTIAL**: exact activity and balance-delta assertions remain task-local |
116
+ | Bridge tracking and details | Completed supported bridge route | Swap/Bridge composition exists; reusable terminal tracking assertions are incomplete | **PARTIAL** |
117
+ | Android multi-network visibility | Imported wallet with holdings on multiple chains | Native Assets read reports aggregate visible holdings without Metro or a development bridge | **AVAILABLE** |
118
+
119
+ ## Platform scope
120
+
121
+ The 135-case source inventory targets Extension. It must not be relabeled as an
122
+ Android checklist. Android release validation uses a separate case inventory
123
+ for the product behaviors exercised on a physical device, while reusing the
124
+ same action vocabulary and report gate where the clients expose equivalent
125
+ flows. Simulator-only evidence is not accepted for Android release claims.