@deeeed/metamask-harness 0.47.4 → 0.48.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 (146) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +3 -2
  3. package/adapters/extension/lib/macos-focus.cjs +2 -2
  4. package/adapters/extension/live.sh +6 -8
  5. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
  6. package/adapters/shared/ensure-runner-deps.sh +6 -0
  7. package/dist/adapters/extension/runtime-decision.js +18 -1
  8. package/dist/adapters/extension/runtime.js +5 -5
  9. package/dist/adapters/extension/surface.js +1 -0
  10. package/dist/app-lifecycle.js +3 -1
  11. package/dist/command-contract.js +2 -0
  12. package/dist/commands/call.js +1 -8
  13. package/dist/commands/device-target.js +12 -3
  14. package/dist/commands/doctor.js +7 -5
  15. package/dist/commands/fixtures.js +1 -1
  16. package/dist/commands/launch/index.js +10 -0
  17. package/dist/commands/manifest.js +3 -1
  18. package/dist/commands/mobile-device-view.js +13 -3
  19. package/dist/commands/parse-args.js +2 -0
  20. package/dist/commands/run-engine.js +29 -4
  21. package/dist/commands/run.js +1 -1
  22. package/dist/commands/runtime-launch.js +10 -1
  23. package/dist/commands/status.js +8 -4
  24. package/dist/devices.js +31 -9
  25. package/dist/execution-provenance.js +8 -7
  26. package/dist/funding-execution-context.js +43 -13
  27. package/dist/heal-bounds.js +3 -3
  28. package/dist/live-adapter-contract.js +4 -0
  29. package/dist/manifest.js +2 -13
  30. package/dist/mm-harness-cli.js +2 -1
  31. package/dist/runner.js +14 -3
  32. package/docs/RECIPES.md +133 -22
  33. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  34. package/library/actions/core/perps/_controller.mjs +10 -0
  35. package/library/actions/core/perps/close_orders.mjs +13 -17
  36. package/library/actions/extension/assets/home-token-identity.mjs +30 -0
  37. package/library/actions/extension/assets/import_custom_token.mjs +61 -79
  38. package/library/actions/extension/assets/open_details.mjs +18 -7
  39. package/library/actions/extension/assets/read_details.mjs +61 -29
  40. package/library/actions/extension/assets/read_visible_state.mjs +4 -1
  41. package/library/actions/extension/assets/select_network_scope.mjs +16 -9
  42. package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
  43. package/library/actions/extension/assets/verify_sorting.mjs +62 -14
  44. package/library/actions/extension/networks/add_custom.mjs +19 -17
  45. package/library/actions/extension/networks/custom_network.mjs +8 -3
  46. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  47. package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
  48. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
  49. package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
  50. package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
  51. package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
  52. package/library/actions/extension/perps/close_visible_position.mjs +3 -3
  53. package/library/actions/extension/perps/edit_margin.mjs +1 -1
  54. package/library/actions/extension/perps/open_balance_action.mjs +16 -7
  55. package/library/actions/extension/perps/perps.mjs +150 -45
  56. package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
  57. package/library/actions/extension/perps/read_snapshot.mjs +2 -1
  58. package/library/actions/extension/perps/read_visible_state.mjs +10 -6
  59. package/library/actions/extension/perps/search_markets.mjs +7 -2
  60. package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
  61. package/library/actions/extension/perps/select_market_filter.mjs +74 -45
  62. package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
  63. package/library/actions/extension/perps/state.mjs +1 -0
  64. package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
  65. package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
  66. package/library/actions/extension/platform/cdp.mjs +23 -142
  67. package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
  68. package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
  69. package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
  70. package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
  71. package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
  72. package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
  73. package/library/actions/extension/ui/navigate.mjs +58 -14
  74. package/library/actions/extension/wallet/lock.mjs +2 -1
  75. package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
  76. package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
  77. package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
  78. package/library/actions/mobile/networks/network-management.mjs +22 -15
  79. package/library/actions/mobile/networks/remove_custom.mjs +6 -2
  80. package/library/actions/mobile/perps/perps.mjs +3 -0
  81. package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
  82. package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
  83. package/library/actions/mobile/platform/native-session.mjs +26 -17
  84. package/library/actions/mobile/platform/observe-ui.mjs +2 -2
  85. package/library/actions/mobile/ui/navigate.mjs +1 -1
  86. package/library/actions/shared/perps/visible-state.mjs +14 -3
  87. package/library/manifests/core.action-manifest.json +14 -2
  88. package/library/manifests/extension.action-manifest.json +49 -16
  89. package/library/manifests/mobile.action-manifest.json +8 -7
  90. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  91. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  92. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  93. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  94. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  95. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  96. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
  97. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  98. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  99. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  100. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  101. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  102. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  103. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  104. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  105. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  106. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  107. package/package.json +3 -3
  108. package/site/assets/help-recipes.json +63 -23
  109. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  110. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  111. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  112. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  113. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  114. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  115. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  116. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  117. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  118. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  119. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  120. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  121. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  122. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  123. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  124. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  125. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  126. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  127. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  128. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  129. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  130. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  131. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  132. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  133. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  134. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  135. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  136. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  137. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  138. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  139. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  140. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  141. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  142. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  143. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  144. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  145. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  146. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
package/dist/devices.js CHANGED
@@ -1,18 +1,30 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
3
- function listConnectedDevices(platform) {
3
+ function reportProbeFailure(errors, platform, error) {
4
+ const code = error?.code;
5
+ if (code === "ENOENT") return;
6
+ const reason = code === "ETIMEDOUT" ? "timed out after 5 seconds" : "failed";
7
+ const probeCode = code ?? error?.status;
8
+ errors.push({
9
+ platform,
10
+ message: `${platform} device discovery ${reason}; device availability is unknown.`,
11
+ userAction: platform === "ios" ? "xcrun simctl list devices booted -j" : "adb devices -l",
12
+ ...probeCode !== void 0 ? { probeCode } : {}
13
+ });
14
+ }
15
+ function listConnectedDevices(platform, errors = []) {
4
16
  const devices = [];
5
- if (!platform || platform === "android") devices.push(...listAndroidDevices());
6
- if (!platform || platform === "ios") devices.push(...listIosSimulators());
17
+ if (!platform || platform === "android") devices.push(...listAndroidDevices(errors));
18
+ if (!platform || platform === "ios") devices.push(...listIosSimulators("booted", errors));
7
19
  return devices;
8
20
  }
9
- function findAvailableIosSimulatorById(id) {
10
- for (const simulator of listIosSimulators("available")) {
21
+ function findAvailableIosSimulatorById(id, errors = []) {
22
+ for (const simulator of listIosSimulators("available", errors)) {
11
23
  if (simulator.id === id) return simulator;
12
24
  }
13
25
  return void 0;
14
26
  }
15
- function listAndroidDevices() {
27
+ function listAndroidDevices(errors) {
16
28
  const adbPath = resolveMobileToolPath("adb");
17
29
  if (!adbPath) return [];
18
30
  let output;
@@ -22,7 +34,12 @@ function listAndroidDevices() {
22
34
  timeout: 5e3,
23
35
  stdio: ["ignore", "pipe", "ignore"]
24
36
  });
25
- } catch {
37
+ } catch (error) {
38
+ reportProbeFailure(errors, "android", error);
39
+ return [];
40
+ }
41
+ if (!output.trimStart().startsWith("List of devices attached")) {
42
+ errors.push({ platform: "android", message: "Android device discovery returned an invalid response; device availability is unknown.", userAction: "adb devices -l" });
26
43
  return [];
27
44
  }
28
45
  const devices = [];
@@ -39,7 +56,7 @@ function listAndroidDevices() {
39
56
  }
40
57
  return devices;
41
58
  }
42
- function listIosSimulators(scope = "booted") {
59
+ function listIosSimulators(scope, errors) {
43
60
  let output;
44
61
  try {
45
62
  output = execFileSync("xcrun", ["simctl", "list", "devices", scope, "-j"], {
@@ -47,13 +64,18 @@ function listIosSimulators(scope = "booted") {
47
64
  timeout: 5e3,
48
65
  stdio: ["ignore", "pipe", "ignore"]
49
66
  });
50
- } catch {
67
+ } catch (error) {
68
+ reportProbeFailure(errors, "ios", error);
51
69
  return [];
52
70
  }
53
71
  let parsed;
54
72
  try {
55
73
  parsed = JSON.parse(output);
74
+ if (!parsed?.devices || typeof parsed.devices !== "object" || Array.isArray(parsed.devices) || Object.values(parsed.devices).some((devices2) => !Array.isArray(devices2) || devices2.some((device) => !device || typeof device.udid !== "string"))) {
75
+ throw new Error("Invalid simctl device inventory");
76
+ }
56
77
  } catch {
78
+ errors.push({ platform: "ios", message: "iOS device discovery returned an invalid response; device availability is unknown.", userAction: `xcrun simctl list devices ${scope} -j` });
57
79
  return [];
58
80
  }
59
81
  const devices = [];
@@ -141,14 +141,15 @@ function sourceSnapshot(root, adapter, excludedRoots = [], includedRoots = []) {
141
141
  "--",
142
142
  ...pathspecs
143
143
  ]);
144
+ const head = gitBuffer(git.topLevel, ["rev-parse", "HEAD"]);
144
145
  return {
145
- head: gitText(git.topLevel, ["rev-parse", "HEAD"]).trim() || null,
146
+ head: head.toString("utf8").trim() || null,
146
147
  status,
147
- sourceFingerprint: adapter === "mobile" && path.resolve(root) === git.topLevel ? mobileSourceFingerprint(root) : nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots)
148
+ sourceFingerprint: adapter === "mobile" && path.resolve(root) === git.topLevel ? mobileSourceFingerprint(root) : nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots, head)
148
149
  };
149
150
  }
150
- function nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots) {
151
- const gitFingerprint = gitSourceFingerprint(git.topLevel, pathspecs);
151
+ function nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots, head) {
152
+ const gitFingerprint = gitSourceFingerprint(git.topLevel, pathspecs, head);
152
153
  if (git.pathspec === "." && includedRoots.length === 0) return gitFingerprint;
153
154
  return createHash("sha256").update(gitFingerprint).update("\0nested-source\0").update(directoryFingerprint(root, excludedRoots, includedRoots)).digest("hex");
154
155
  }
@@ -169,16 +170,16 @@ function gitContext(root) {
169
170
  return null;
170
171
  }
171
172
  }
172
- function gitSourceFingerprint(topLevel, pathspecs) {
173
+ function gitSourceFingerprint(topLevel, pathspecs, head) {
173
174
  const hash = createHash("sha256");
174
- hash.update(gitBuffer(topLevel, ["rev-parse", "HEAD"]));
175
+ hash.update(head);
175
176
  hash.update("\0diff\0");
176
177
  hash.update(
177
178
  gitBuffer(topLevel, [
178
179
  "diff",
179
180
  "--no-ext-diff",
180
181
  "--binary",
181
- "HEAD",
182
+ head.toString("utf8").trim(),
182
183
  "--",
183
184
  ...pathspecs
184
185
  ])
@@ -4,6 +4,7 @@ import { chmod, lstat, mkdir, open, readFile, readdir, realpath } from "node:fs/
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { digestRecipeDocument } from "@farmslot/protocol";
7
+ import { recipeRuntimeDir } from "./paths.js";
7
8
  const SHA256 = /^[a-f0-9]{64}$/u;
8
9
  const INTEGER = /^(0|[1-9][0-9]{0,39})$/u;
9
10
  const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
@@ -99,7 +100,7 @@ async function loadTrustedMutationBase(options) {
99
100
  fundingBalanceProof
100
101
  );
101
102
  const identity = await validateFundingDocuments(documents);
102
- const currentReleaseRuntime = options.testIo?.currentReleaseRuntime ?? releaseRuntimeValidator(path.resolve(options.projectRoot));
103
+ const currentReleaseRuntime = options.testIo?.currentReleaseRuntime ?? releaseRuntimeValidator(path.resolve(options.projectRoot), identity.proofLane);
103
104
  const currentRelease = await currentReleaseRuntime(false, identity.cdpPort);
104
105
  if (currentRelease.artifactSha256 !== identity.subjectDigest || currentRelease.treeSha256 !== identity.releaseTreeSha256) {
105
106
  throw new Error("Runner-owned current release artifact does not match the funding release proof.");
@@ -162,6 +163,7 @@ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
162
163
  }
163
164
  }
164
165
  if (actual.size === 0) throw new Error("Funding documents require a resolved external-mutation plan.");
166
+ await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
165
167
  const artifactsIdentitySha256 = await directoryIdentity(source.artifactsDir);
166
168
  const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
167
169
  const runId = sha256(canonical({
@@ -719,14 +721,27 @@ async function validateFundingEvidenceChain(documents, identity) {
719
721
  if (policy.required !== true || policy.registrySha256 !== identity.registrySha256 || policy.profileId !== identity.profileId || policy.domain !== identity.domain) {
720
722
  throw new Error("Funding release proof does not bind the runner-trusted funding policy.");
721
723
  }
722
- const artifact = requireRecord(releaseProof.artifact, "funding release proof artifact");
723
- if (artifact.sha256 !== identity.subjectDigest || !SHA256.test(String(artifact.treeSha256 ?? ""))) {
724
- throw new Error("Funding release proof artifact identity is invalid.");
724
+ const sourceDev = identity.proofLane === "source-dev";
725
+ let releaseTreeSha256;
726
+ if (sourceDev) {
727
+ const source = requireRecord(releaseProof.source, "funding source-dev proof source");
728
+ const workingTree = requireRecord(source.workingTree, "funding source-dev working tree");
729
+ const build = requireRecord(source.build, "funding source-dev build");
730
+ if (identity.profileId !== "live-perps-tiny" || identity.domain !== "perps" || "artifact" in releaseProof || !/^[a-f0-9]{40}$/u.test(String(source.commit ?? "")) || !["clean", "dirty"].includes(String(workingTree.state)) || !SHA256.test(String(workingTree.statusSha256 ?? "")) || (workingTree.state === "clean" ? workingTree.statusSha256 !== sha256("") || "diffSha256" in workingTree : !SHA256.test(String(workingTree.diffSha256 ?? ""))) || build.outputSha256 !== identity.subjectDigest || !Array.isArray(build.instrumentation) || build.instrumentation.some((item) => typeof item !== "string" || !item.trim()) || releaseProof.compatibilityOnly !== true || releaseProof.releaseRecommendationEligible !== false) {
731
+ throw new Error("Funding source-dev proof requires bound build provenance and compatibility-only evidence.");
732
+ }
733
+ releaseTreeSha256 = String(build.outputSha256);
734
+ } else {
735
+ const artifact = requireRecord(releaseProof.artifact, "funding release proof artifact");
736
+ if (artifact.sha256 !== identity.subjectDigest || !SHA256.test(String(artifact.treeSha256 ?? ""))) {
737
+ throw new Error("Funding release proof artifact identity is invalid.");
738
+ }
739
+ releaseTreeSha256 = String(artifact.treeSha256);
725
740
  }
726
741
  const runtime = requireRecord(releaseProof.runtime, "funding release proof runtime");
727
742
  const extensionRuntime = requireRecord(runtime.extension, "funding release proof Extension runtime");
728
- if (identity.client !== "extension" || identity.proofLane !== "official-unmodified" || runtime.fixtures !== false || runtime.instrumentation !== false || extensionRuntime.cleanProfile !== true || extensionRuntime.runtimeTreeSha256 !== artifact.treeSha256) {
729
- throw new Error("Funding release proof is not an unmodified clean Extension runtime.");
743
+ if (identity.client !== "extension" || extensionRuntime.runtimeTreeSha256 !== releaseTreeSha256 || (sourceDev ? typeof runtime.fixtures !== "boolean" || typeof runtime.instrumentation !== "boolean" || typeof extensionRuntime.cleanProfile !== "boolean" || typeof extensionRuntime.profileId !== "string" || !extensionRuntime.profileId.trim() : identity.proofLane !== "official-unmodified" || runtime.fixtures !== false || runtime.instrumentation !== false || extensionRuntime.cleanProfile !== true)) {
744
+ throw new Error("Funding proof does not match its permitted Extension runtime lane.");
730
745
  }
731
746
  const onboarding = requireRecord(releaseProof.walletOnboarding, "funding release proof walletOnboarding");
732
747
  const onboardingRuntime = requireRecord(onboarding.runtime, "funding release proof wallet runtime");
@@ -837,8 +852,8 @@ async function validateFundingEvidenceChain(documents, identity) {
837
852
  const onboardingWallet = successfulWalletOutput(
838
853
  onboardingTrace,
839
854
  "funding release onboarding evidence",
840
- "ui-import",
841
- "extension-visible-onboarding-import"
855
+ sourceDev ? "ui-visible-session" : "ui-import",
856
+ sourceDev ? "extension-visible-wallet-session" : "extension-visible-onboarding-import"
842
857
  );
843
858
  const onboardingOrigin = extensionOrigin(onboardingWallet.targetUrl, "funding release onboarding wallet");
844
859
  if (onboardingWallet.runtimeSessionId !== onboardingRuntime.runtimeSessionId || onboardingWallet.cdpPort !== onboardingRuntime.cdpPort || onboardingWallet.cdpTargetId !== onboardingRuntime.cdpTargetId || extensionOrigin(onboardingRuntime.targetUrl, "funding release onboarding runtime") !== onboardingOrigin) {
@@ -972,7 +987,7 @@ async function validateFundingEvidenceChain(documents, identity) {
972
987
  evidenceDocument.runtimeSessionId,
973
988
  "funding balance evidence runtimeSessionId"
974
989
  );
975
- if (evidenceDocument.selectedIdentitySha256 !== onboardingWallet.selectedIdentitySha256 || wallet.selectedIdentitySha256 !== onboardingWallet.selectedIdentitySha256 || wallet.runtimeSessionId !== runtimeSessionId || wallet.cdpPort !== onboardingWallet.cdpPort || extensionOrigin(wallet.targetUrl, "funding balance wallet") !== onboardingOrigin) {
990
+ if (evidenceDocument.selectedIdentitySha256 !== onboardingWallet.selectedIdentitySha256 || wallet.selectedIdentitySha256 !== onboardingWallet.selectedIdentitySha256 || wallet.runtimeSessionId !== runtimeSessionId || sourceDev && (wallet.runtimeSessionId !== onboardingWallet.runtimeSessionId || wallet.cdpTargetId !== onboardingWallet.cdpTargetId) || wallet.cdpPort !== onboardingWallet.cdpPort || extensionOrigin(wallet.targetUrl, "funding balance wallet") !== onboardingOrigin) {
976
991
  throw new Error("Funding balance evidence wallet or runtime is not anchored to release onboarding.");
977
992
  }
978
993
  const currentRuntime = {
@@ -999,13 +1014,26 @@ async function validateFundingEvidenceChain(documents, identity) {
999
1014
  return {
1000
1015
  requirements,
1001
1016
  runtime: balanceRuntime,
1002
- releaseTreeSha256: String(artifact.treeSha256)
1017
+ releaseTreeSha256
1003
1018
  };
1004
1019
  }
1005
- function releaseRuntimeValidator(projectRoot) {
1020
+ function releaseRuntimeValidator(projectRoot, proofLane) {
1006
1021
  return async (requireLive, cdpPort) => {
1007
1022
  const { extensionRuntimeReusable } = await import("./commands/launch/extension.js");
1008
1023
  const { releaseArtifactState, runtimeDistCheck } = await import("./adapters/extension/runtime-decision.js");
1024
+ if (proofLane === "source-dev") {
1025
+ const { extensionTreeSha256 } = await import("./adapters/extension/artifact-integrity.js");
1026
+ const configuredPort = Number(process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
1027
+ if (configuredPort !== cdpPort || !await extensionRuntimeReusable(projectRoot, { status: "none" })) {
1028
+ throw new Error("Source-dev funding requires the current harness-owned Extension runtime.");
1029
+ }
1030
+ const treeSha256 = extensionTreeSha256(path.join(
1031
+ projectRoot,
1032
+ recipeRuntimeDir(),
1033
+ process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist"
1034
+ ));
1035
+ return { artifactSha256: treeSha256, treeSha256 };
1036
+ }
1009
1037
  const artifact = releaseArtifactState(projectRoot);
1010
1038
  if (artifact.status !== "valid" || runtimeDistCheck(projectRoot, artifact).status !== "fresh" || !artifact.sha256 || !artifact.treeSha256) {
1011
1039
  throw new Error("Runner-owned current release artifact is invalid or stale.");
@@ -1253,14 +1281,16 @@ function parsePreflightRequirements(value) {
1253
1281
  if (!Array.isArray(value)) throw new Error("Funding preflight requirements must be an array.");
1254
1282
  return value.map((entry, index) => {
1255
1283
  const record = requireRecord(entry, `funding preflight requirements[${index}]`);
1284
+ const evidence = requireRecord(record.evidence, `funding preflight requirements[${index}].evidence`);
1285
+ const wrapper = requireRecord(evidence.wrapper, `funding preflight requirements[${index}].evidence.wrapper`);
1256
1286
  return {
1257
1287
  kind: requireIdentifier(record.kind, `funding preflight requirements[${index}].kind`),
1258
1288
  id: requireIdentifier(record.id, `funding preflight requirements[${index}].id`),
1259
1289
  minBaseUnits: requireInteger(record.minBaseUnits, `funding preflight requirements[${index}].minBaseUnits`),
1260
1290
  actualBaseUnits: requireInteger(record.actualBaseUnits, `funding preflight requirements[${index}].actualBaseUnits`),
1261
1291
  evidenceSha256: requireIdentifier(
1262
- record.evidenceSha256,
1263
- `funding preflight requirements[${index}].evidenceSha256`,
1292
+ wrapper.sha256,
1293
+ `funding preflight requirements[${index}].evidence.wrapper.sha256`,
1264
1294
  true
1265
1295
  )
1266
1296
  };
@@ -63,7 +63,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
63
63
  function classifyFailure(output) {
64
64
  if (/SCREENSHOT_PROTECTED|screenshot is protected by FLAG_SECURE/iu.test(output)) return "screenshot-protected";
65
65
  if (/CDP_TIMEOUT|Mobile CDP bridge command timed out|NO_TARGET|WS_CLOSED/iu.test(output)) return "infra";
66
- if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
66
+ if (/fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
67
67
  if (/metro|cdp|chrome|bridge|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
68
68
  return "infra";
69
69
  }
@@ -101,8 +101,8 @@ function checkHealBounds(target, output, state) {
101
101
  return {
102
102
  code: "WALLET_STATE_REQUIRED",
103
103
  exitCode: EXIT.bounded,
104
- message: "recovery would require a seeded wallet \u2014 healing never touches fixtures.",
105
- userAction: "run mm-harness fixtures set",
104
+ message: "wallet prerequisites need attention; healing never changes wallet state.",
105
+ userAction: /(?:^|\n)\s*Next:\s*([^\r\n]+)/u.exec(output)?.[1] ?? "run mm-harness doctor --json",
106
106
  originalError
107
107
  };
108
108
  }
@@ -334,6 +334,10 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
334
334
  throw new Error(`Live adapter ${script} timed out after ${processTimeoutMs}ms.`);
335
335
  }
336
336
  if (result.exitCode !== 0) {
337
+ const failureOutput = await readFile(outputPath, "utf8").then(JSON.parse).catch(() => null);
338
+ if (Array.isArray(failureOutput?.artifacts)) {
339
+ for (const artifact of failureOutput.artifacts) context.registerArtifact(artifact);
340
+ }
337
341
  throw new Error(`Live adapter ${script} exited ${result.exitCode}: ${result.stderr || result.stdout}`);
338
342
  }
339
343
  let parsed = null;
package/dist/manifest.js CHANGED
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
4
4
  import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
5
- import { actionSourceIntegrity } from "./recipe-security.js";
6
5
  function loadMetaMaskMobileActionManifest() {
7
6
  return asActionManifest(readJson(manifestPath("mobile")));
8
7
  }
@@ -26,7 +25,6 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
26
25
  );
27
26
  const manifest = loadActionManifest(adapter, manifestPath2);
28
27
  await validateManifest(manifest);
29
- const integrity = await actionSourceIntegrity(manifestPath2, "task", implementationRoot);
30
28
  return {
31
29
  manifest,
32
30
  actionSources: await describeManifestSources(manifest, {
@@ -34,8 +32,7 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
34
32
  tier: "task",
35
33
  manifestPath: manifestPath2,
36
34
  implementationRoot,
37
- trust: "unknown",
38
- ...integrity
35
+ trust: "unknown"
39
36
  })
40
37
  };
41
38
  }
@@ -67,12 +64,6 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
67
64
  for (const [name, entry] of Object.entries(canonicalActions)) {
68
65
  if (official.has(name)) mergedActions[name] = entry;
69
66
  }
70
- const canonicalImplementationRoot = path.join(canonical.source.root, "actions");
71
- const canonicalIntegrity = await actionSourceIntegrity(
72
- canonical.file,
73
- "canonical",
74
- canonicalImplementationRoot
75
- );
76
67
  const actionSources = /* @__PURE__ */ new Map();
77
68
  for (const name of Object.keys(mergedActions)) {
78
69
  actionSources.set(name, {
@@ -96,14 +87,12 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
96
87
  const sourceName = isCanonical ? "metamask" : entry.source.name ?? path.basename(entry.source.root);
97
88
  const tier = isCanonical ? "canonical" : sourceName === "personal" ? "personal" : "team";
98
89
  const implementationRoot = path.join(entry.source.root, "actions");
99
- const integrity = isCanonical ? canonicalIntegrity : await actionSourceIntegrity(entry.file, tier, implementationRoot);
100
90
  const sourceInfo = {
101
91
  name: sourceName,
102
92
  tier,
103
93
  manifestPath: entry.file,
104
94
  implementationRoot,
105
- trust: entry.source.provenance?.trust ?? (isCanonical ? "trusted" : "unknown"),
106
- ...integrity
95
+ trust: entry.source.provenance?.trust ?? (isCanonical ? "trusted" : "unknown")
107
96
  };
108
97
  for (const [name, action] of Object.entries(actionsOf(entry.manifest))) {
109
98
  if (official.has(name)) {
@@ -288,7 +288,7 @@ Advanced integrations:
288
288
 
289
289
  Example:
290
290
  mm-harness run wallet.smoke --describe
291
- mm-harness run perps.clean-market-testnet market=BTC --plan --adapter mobile
291
+ mm-harness run perps.release-market-search query=Bitcoin --plan --adapter mobile
292
292
  mm-harness run recipe.json market=ETH --adapter extension`
293
293
  },
294
294
  {
@@ -613,6 +613,7 @@ Example:
613
613
  --device <serial> Explicit Android device serial
614
614
  --cdp-port <port> Extension CDP port
615
615
  --chrome-user-data-dir <dir> Isolated Chrome profile path
616
+ --reset-profile Explicitly clear that profile; default preserves wallet state
616
617
  --artifacts-dir <dir> Validation evidence directory
617
618
  --start-watch Build through the checkout watcher
618
619
  --build-lavamoat Build through the production-like path
package/dist/runner.js CHANGED
@@ -20,10 +20,11 @@ import {
20
20
  bindMetaMaskMobileLifecycleDeadline,
21
21
  createMetaMaskMobileLifecycleCommandRunner,
22
22
  MOBILE_LIFECYCLE_DEADLINE_FIELD,
23
+ MOBILE_LIFECYCLE_PREPARED,
23
24
  resolveMetaMaskMobileLifecycleTarget
24
25
  } from "./app-lifecycle.js";
25
26
  import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
26
- import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
27
+ import { actionSourceIntegrity, metaMaskActionExecutionCapabilities } from "./recipe-security.js";
27
28
  import {
28
29
  prepareLiveAdapterScript
29
30
  } from "./live-adapter-contract.js";
@@ -210,7 +211,7 @@ function withTrustedMutationContext(adapters, base, externalMutationActions, ada
210
211
  ...entry,
211
212
  async execute(node, context) {
212
213
  const identityProof = entry.action === identityProofAction;
213
- const invalidatesIdentity = identityInvalidatingActions.has(entry.action) || entry.action.startsWith("ui.") && !identityPreservingUiActions.has(entry.action);
214
+ const invalidatesIdentity = identityInvalidatingActions.has(entry.action) || base?.proofLane !== "source-dev" && entry.action.startsWith("ui.") && !identityPreservingUiActions.has(entry.action);
214
215
  if (identityProof || invalidatesIdentity) selectedIdentitySha256 = void 0;
215
216
  const externalMutation = externalMutationActions.has(entry.action);
216
217
  if (externalMutation && !base) {
@@ -272,6 +273,15 @@ async function prepareLiveAdapterImplementations(platform, actions, sources, tru
272
273
  source.digest = executable.sourceDigest;
273
274
  source.resolveDigest = void 0;
274
275
  }));
276
+ for (const source of sources.values()) {
277
+ if (source.tier === "official" || source.digest) continue;
278
+ const integrity = await actionSourceIntegrity(source.manifestPath, source.tier, source.implementationRoot);
279
+ for (const candidate of sources.values()) {
280
+ if (!candidate.digest && candidate.manifestPath === source.manifestPath && candidate.implementationRoot === source.implementationRoot && candidate.tier === source.tier) {
281
+ Object.assign(candidate, integrity);
282
+ }
283
+ }
284
+ }
275
285
  return prepared;
276
286
  }
277
287
  function adapterSecurity(action, sources, trustTaskActions = false) {
@@ -405,7 +415,8 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
405
415
  command: "foreground",
406
416
  settle_ms: 0,
407
417
  timeout_ms: foregroundTimeoutMs,
408
- [MOBILE_LIFECYCLE_DEADLINE_FIELD]: readinessDeadline
418
+ [MOBILE_LIFECYCLE_DEADLINE_FIELD]: readinessDeadline,
419
+ [MOBILE_LIFECYCLE_PREPARED]: true
409
420
  },
410
421
  context
411
422
  );