@deeeed/metamask-harness 0.79.0 → 0.81.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 (252) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +16 -64
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  21. package/adapters/shared/open-debug.mjs +2 -2
  22. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  23. package/adapters/terminal/cleanup.mjs +2 -1
  24. package/adapters/terminal/inject.mjs +2 -1
  25. package/adapters/terminal/launch.mjs +17 -13
  26. package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
  27. package/adapters/terminal/lib/readiness.mjs +20 -12
  28. package/adapters/terminal/policy.mjs +35 -0
  29. package/adapters/terminal/verify.mjs +10 -3
  30. package/adapters/terminal/wallet-host.mjs +43 -225
  31. package/bin/mm-harness +9 -28
  32. package/dist/adapters/checkout-files.js +16 -0
  33. package/dist/adapters/console-capture.js +14 -1
  34. package/dist/adapters/core/actions.js +8 -4
  35. package/dist/adapters/core/overlay.js +1 -1
  36. package/dist/adapters/core/readiness.js +28 -1
  37. package/dist/adapters/core/run.js +2 -2
  38. package/dist/adapters/core/surface.js +16 -1
  39. package/dist/adapters/extension/actions.js +6 -2
  40. package/dist/adapters/extension/browser-pid.js +56 -0
  41. package/dist/adapters/extension/console-capture.js +1 -1
  42. package/dist/adapters/extension/doctor-checks.js +1 -1
  43. package/dist/adapters/extension/ensure-ready.js +5 -4
  44. package/dist/adapters/extension/extension-id.js +1 -1
  45. package/dist/adapters/extension/feature-flags.js +1 -1
  46. package/dist/adapters/extension/fixtures.js +1 -2
  47. package/dist/adapters/extension/launch.js +4 -5
  48. package/dist/adapters/extension/launched-browser.js +1 -2
  49. package/dist/adapters/extension/network-observer.js +14 -1
  50. package/dist/adapters/extension/overlay.js +1 -1
  51. package/dist/adapters/extension/product-config.js +1 -1
  52. package/dist/adapters/extension/readiness.js +1 -1
  53. package/dist/adapters/extension/reload.js +1 -1
  54. package/dist/adapters/extension/run.js +135 -14
  55. package/dist/adapters/extension/runtime-decision.js +2 -1
  56. package/dist/adapters/extension/runtime.js +9 -5
  57. package/dist/adapters/extension/surface.js +15 -2
  58. package/dist/adapters/extension/verify.js +2 -2
  59. package/dist/adapters/harness-freshness.js +2 -1
  60. package/dist/adapters/mobile/actions.js +4 -1
  61. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  62. package/dist/adapters/mobile/app-identity.js +10 -0
  63. package/dist/adapters/mobile/doctor-checks.js +1 -1
  64. package/dist/adapters/mobile/fixtures.js +1 -1
  65. package/dist/adapters/mobile/launch.js +27 -28
  66. package/dist/adapters/mobile/lifecycle.js +1 -1
  67. package/dist/adapters/mobile/metro-env.js +9 -59
  68. package/dist/adapters/mobile/network-observer.js +68 -0
  69. package/dist/adapters/mobile/performance-observer.js +2 -4
  70. package/dist/adapters/mobile/prepare-target.js +1 -1
  71. package/dist/adapters/mobile/prepare.js +3 -4
  72. package/dist/adapters/mobile/provision.js +1 -1
  73. package/dist/adapters/mobile/readiness.js +1 -1
  74. package/dist/adapters/mobile/recording-target.js +15 -0
  75. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  76. package/dist/adapters/mobile/reload.js +1 -1
  77. package/dist/adapters/mobile/run.js +39 -15
  78. package/dist/adapters/mobile/runtime-decision.js +2 -4
  79. package/dist/adapters/mobile/source-freshness.js +22 -126
  80. package/dist/adapters/mobile/surface.js +26 -3
  81. package/dist/adapters/mobile/ui.js +16 -5
  82. package/dist/adapters/mobile/verify.js +2 -3
  83. package/dist/adapters/performance/cdp-trace.js +1 -1
  84. package/dist/adapters/performance/device-platform.js +1 -1
  85. package/dist/adapters/perps-order-inputs.js +36 -0
  86. package/dist/adapters/proof-failure.js +11 -0
  87. package/dist/adapters/slot-ports.js +1 -1
  88. package/dist/adapters/surface.js +1 -0
  89. package/dist/adapters/terminal/actions.js +4 -1
  90. package/dist/adapters/terminal/console-capture.js +1 -1
  91. package/dist/adapters/terminal/doctor-checks.js +2 -0
  92. package/dist/adapters/terminal/launch.js +4 -1
  93. package/dist/adapters/terminal/overlay.js +8 -1
  94. package/dist/adapters/terminal/run.js +33 -5
  95. package/dist/adapters/terminal/surface.js +9 -1
  96. package/dist/adapters.js +27 -7
  97. package/dist/advisor-policy.js +1 -1
  98. package/dist/checkout-view.js +1 -2
  99. package/dist/cli.js +59 -21
  100. package/dist/command-contract.js +1 -26
  101. package/dist/commands/check.js +33 -10
  102. package/dist/commands/checklist.js +1 -1
  103. package/dist/commands/completion-candidates.js +6 -14
  104. package/dist/commands/config.js +2 -2
  105. package/dist/commands/core-readiness.js +12 -7
  106. package/dist/commands/debug.js +3 -3
  107. package/dist/commands/device-target.js +5 -5
  108. package/dist/commands/doctor.js +2 -14
  109. package/dist/commands/domain.js +2 -2
  110. package/dist/commands/ensure-ready.js +1 -7
  111. package/dist/commands/fixtures.js +5 -6
  112. package/dist/commands/help.js +3 -4
  113. package/dist/commands/launch/device.js +10 -17
  114. package/dist/commands/launch/extension.js +2 -8
  115. package/dist/commands/launch/mobile.js +1 -1
  116. package/dist/commands/logs.js +3 -3
  117. package/dist/commands/mobile-device-view.js +4 -2
  118. package/dist/commands/overlay.js +110 -0
  119. package/dist/commands/pin.js +1 -1
  120. package/dist/commands/pr-body.js +2 -2
  121. package/dist/commands/prepare.js +4 -9
  122. package/dist/commands/provision.js +3 -12
  123. package/dist/commands/recipe-advice.js +1 -2
  124. package/dist/commands/recipe-quality.js +2 -3
  125. package/dist/commands/resolve-extension.js +1 -7
  126. package/dist/commands/review.js +4 -4
  127. package/dist/commands/runtime-decision.js +1 -6
  128. package/dist/commands/runtime-health.js +1 -7
  129. package/dist/commands/runtime-launch.js +1 -13
  130. package/dist/commands/self-test.js +5 -8
  131. package/dist/commands/status-watch.js +1 -2
  132. package/dist/commands/status.js +2 -5
  133. package/dist/commands/task-init.js +2 -3
  134. package/dist/commands/tutorial.js +2 -2
  135. package/dist/commands/update.js +1 -1
  136. package/dist/completions-cache.js +1 -1
  137. package/dist/console-classifier.js +19 -0
  138. package/dist/doctor.js +2 -3
  139. package/dist/engine.js +57 -0
  140. package/dist/funding-execution-context.js +96 -1
  141. package/dist/host.js +32 -0
  142. package/dist/index.js +1 -6
  143. package/dist/live-tsx.js +49 -0
  144. package/dist/mm-harness-cli.js +4 -6
  145. package/dist/paths.js +34 -64
  146. package/dist/performance-observation.js +3 -5
  147. package/dist/progress.js +1 -1
  148. package/dist/recipe-assistance-context.js +6 -9
  149. package/dist/recipe-security.js +2 -59
  150. package/dist/review/knowledge.js +2 -2
  151. package/dist/runner.js +7 -4
  152. package/dist/runtime-context.js +1 -1
  153. package/dist/task-view.js +2 -3
  154. package/docs/CONTRIBUTING.md +5 -3
  155. package/docs/RECIPES.md +31 -3
  156. package/docs/cli-contract.md +2 -2
  157. package/docs/feature-guard.md +90 -0
  158. package/library/README.md +3 -1
  159. package/library/actions/core/perps/_controller.mjs +153 -134
  160. package/library/actions/core/perps/_rewards.mjs +186 -0
  161. package/library/actions/core/perps/close_positions.mjs +1 -1
  162. package/library/actions/core/perps/place_order.mjs +167 -45
  163. package/library/actions/core/perps/quote_fees.mjs +61 -0
  164. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  165. package/library/actions/extension/feature_flags/state.mjs +1 -1
  166. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  167. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  168. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  169. package/library/actions/extension/perps/perps.mjs +320 -3
  170. package/library/actions/extension/perps/place_order.mjs +3 -1
  171. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  172. package/library/actions/extension/platform/cdp.mjs +73 -11
  173. package/library/actions/harness-exports.mjs +1 -6
  174. package/library/actions/mobile/app/network-control.mjs +6 -8
  175. package/library/actions/mobile/perps/perps.mjs +216 -11
  176. package/library/actions/mobile/perps/place_order.mjs +3 -1
  177. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  178. package/library/actions/mobile/platform/bridge.mjs +8 -8
  179. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  180. package/library/actions/mobile/platform/native-session.mjs +3 -4
  181. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  182. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  183. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  184. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  185. package/library/actions/shared/console-findings.mjs +1 -1
  186. package/library/actions/shared/perps/order-policy.mjs +177 -0
  187. package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
  188. package/library/actions/terminal/app/launch.mjs +3 -2
  189. package/library/actions/terminal/platform/hud.mjs +2 -1
  190. package/library/actions/terminal/platform/page.mjs +2 -1
  191. package/library/actions/terminal/platform/runtime.mjs +2 -1
  192. package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
  193. package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
  194. package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
  195. package/library/manifests/core.action-manifest.json +87 -13
  196. package/library/manifests/extension.action-manifest.json +4 -0
  197. package/library/manifests/mobile.action-manifest.json +12 -0
  198. package/package.json +8 -9
  199. package/site/assets/help-recipes.json +1 -0
  200. package/adapters/extension/lib/slot-title.cjs +0 -292
  201. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  202. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  203. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  204. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  205. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  206. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  207. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  208. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  209. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  210. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  211. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  212. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  213. package/adapters/mobile/launch-metro.cjs +0 -77
  214. package/adapters/mobile/metro-config.cjs +0 -98
  215. package/adapters/mobile/metro-log-generation.cjs +0 -106
  216. package/adapters/terminal/lib/origin.mjs +0 -30
  217. package/adapters/terminal/lib/page-script.mjs +0 -246
  218. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  219. package/dist/adapters/mobile/frame-metrics.js +0 -118
  220. package/dist/adapters/mobile/video-recorder.js +0 -289
  221. package/dist/artifact-files.js +0 -92
  222. package/dist/checkout-lock.js +0 -149
  223. package/dist/cli-color.js +0 -98
  224. package/dist/command-journal.js +0 -308
  225. package/dist/commands/call.js +0 -685
  226. package/dist/commands/last.js +0 -60
  227. package/dist/commands/launch/index.js +0 -109
  228. package/dist/commands/list-executables.js +0 -247
  229. package/dist/commands/manifest.js +0 -488
  230. package/dist/commands/parse-args.js +0 -279
  231. package/dist/commands/reload.js +0 -41
  232. package/dist/commands/run-engine.js +0 -1340
  233. package/dist/commands/run-report.js +0 -241
  234. package/dist/commands/run.js +0 -751
  235. package/dist/commands/shared.js +0 -323
  236. package/dist/commands/stop.js +0 -60
  237. package/dist/devices.js +0 -100
  238. package/dist/discovery.js +0 -15
  239. package/dist/execution-provenance.js +0 -332
  240. package/dist/harness.js +0 -451
  241. package/dist/heal-bounds.js +0 -137
  242. package/dist/json-stream.js +0 -60
  243. package/dist/leaf-invoke.js +0 -19
  244. package/dist/library-provenance.js +0 -33
  245. package/dist/live-adapter-contract.js +0 -380
  246. package/dist/metamask-action-validation.js +0 -68
  247. package/dist/network-observation.js +0 -290
  248. package/dist/recording-target.js +0 -120
  249. package/dist/run-diagnostics.js +0 -364
  250. package/dist/run-recording.js +0 -481
  251. package/dist/runtime-proof.js +0 -90
  252. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -1,1340 +0,0 @@
1
- import fs from "node:fs";
2
- import { mkdtemp } from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { validateRuntimeProofPlan } from "../runtime-proof.js";
6
- import {
7
- ensureOverlay,
8
- newHealState,
9
- parseHeal,
10
- recipeRunning,
11
- checkHealBounds,
12
- conciseFailureForHuman
13
- } from "../heal-bounds.js";
14
- import { resolveActionManifest, validateManifest } from "../manifest.js";
15
- import { getAdapterSurface } from "../adapters/surface.js";
16
- import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
17
- import {
18
- importRecipeHarness,
19
- importRecipeProtocol,
20
- runnerDir,
21
- recipeRuntimePath
22
- } from "../paths.js";
23
- import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
24
- import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
25
- import {
26
- resolveMetaMaskParameterValue,
27
- validateMetaMaskActionInputs
28
- } from "../metamask-action-validation.js";
29
- import { gitLibraryProvenance } from "../library-provenance.js";
30
- import { runnableLibraryRecipes } from "../discovery.js";
31
- import { recipeComposition } from "@farmslot/recipe-cli";
32
- import {
33
- captureExecutionProvenance,
34
- executionProvenanceDrift,
35
- ProvenanceDriftError,
36
- writeExecutionProvenance
37
- } from "../execution-provenance.js";
38
- import {
39
- authorizeTrustedMutationPlan,
40
- loadTrustedMutationBase
41
- } from "../funding-execution-context.js";
42
- import {
43
- beginRunDiagnostics,
44
- finishRunDiagnostics
45
- } from "../run-diagnostics.js";
46
- import { executedBrowser, recipeCdpPorts } from "./run-report.js";
47
- import { EXIT } from "./shared.js";
48
- import {
49
- actionManifestPathOption,
50
- optionString,
51
- optionFlag,
52
- optionStrings,
53
- isRecord,
54
- shellQuoteArg,
55
- targetPath,
56
- usageError
57
- } from "./parse-args.js";
58
- async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution, effectsState = newHealState()) {
59
- const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
60
- adapter,
61
- projectRoot,
62
- runtimeOptions
63
- );
64
- try {
65
- const wasPreflighted = preflightedExecution !== void 0;
66
- const execution = preflightedExecution ?? await resolveRecipeExecution(
67
- adapter,
68
- recipe,
69
- artifactsDir,
70
- projectRoot,
71
- actionManifestPath,
72
- runtimeOptions
73
- );
74
- const {
75
- runner,
76
- runRequest,
77
- absoluteArtifactsDir,
78
- useFramedExtensionRecording,
79
- provenanceInput,
80
- provenanceSnapshots
81
- } = execution;
82
- if (!wasPreflighted) await runner.preflight(runRequest);
83
- await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
84
- await runner.preflight(runRequest);
85
- const preExecute = await captureExecutionProvenance(
86
- provenanceInput,
87
- "pre-execute"
88
- );
89
- provenanceSnapshots.push(preExecute);
90
- const preExecuteDrift = executionProvenanceDrift(
91
- provenanceSnapshots[0],
92
- preExecute
93
- );
94
- if (preExecuteDrift.length > 0) {
95
- const provenancePath2 = await writeExecutionProvenance(
96
- absoluteArtifactsDir,
97
- provenanceSnapshots,
98
- preExecuteDrift
99
- );
100
- throw new ProvenanceDriftError(provenancePath2, preExecuteDrift);
101
- }
102
- const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
103
- const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
104
- record: true,
105
- cdpPort: runtimeOptions.cdpPort
106
- }) : void 0;
107
- let result;
108
- let executionError;
109
- try {
110
- result = await runner.run(runRequest);
111
- } catch (error) {
112
- executionError = error;
113
- try {
114
- await stopRecipeRecording(recording);
115
- } catch (recordingError2) {
116
- console.error(
117
- `WARN: recipe and video recording both failed; preserving recipe failure: ${recordingError2 instanceof Error ? recordingError2.message : String(recordingError2)}`
118
- );
119
- }
120
- } finally {
121
- const teardown = getAdapterSurface(adapter).run?.teardown;
122
- if (teardown) await teardown(projectRoot, recipeRunEnv(adapter, runtimeOptions));
123
- }
124
- let recordingError;
125
- if (executionError === void 0 && result) {
126
- try {
127
- await stopRecipeRecording(recording, result);
128
- } catch (error) {
129
- recordingError = error;
130
- }
131
- }
132
- const end = await captureExecutionProvenance(provenanceInput, "end");
133
- provenanceSnapshots.push(end);
134
- const endDrift = executionProvenanceDrift(provenanceSnapshots[0], end);
135
- const provenancePath = await writeExecutionProvenance(
136
- absoluteArtifactsDir,
137
- provenanceSnapshots,
138
- endDrift,
139
- result?.artifactManifestPath
140
- );
141
- if (endDrift.length > 0) {
142
- throw new ProvenanceDriftError(provenancePath, endDrift, executionError);
143
- }
144
- if (executionError !== void 0) throw executionError;
145
- if (recordingError !== void 0) throw recordingError;
146
- if (!result) throw new Error("Recipe execution returned no result.");
147
- const finalized = await finishRunDiagnostics(diagnosticBaseline, result);
148
- persistRunEffects(
149
- finalized.summaryPath,
150
- finalized.artifactManifestPath,
151
- effectsState
152
- );
153
- const browser = executedBrowser(
154
- adapter,
155
- projectRoot,
156
- finalized.artifactManifestPath,
157
- recipeCdpPorts(runRequest)
158
- );
159
- return browser ? { ...finalized, browser } : finalized;
160
- } finally {
161
- restoreRuntimeEnvironment();
162
- }
163
- }
164
- async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
165
- const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
166
- adapter,
167
- projectRoot,
168
- runtimeOptions
169
- );
170
- try {
171
- const execution = await resolveRecipeExecution(
172
- adapter,
173
- recipe,
174
- artifactsDir,
175
- projectRoot,
176
- actionManifestPath,
177
- runtimeOptions
178
- );
179
- await execution.runner.preflight(execution.runRequest);
180
- return execution;
181
- } finally {
182
- restoreRuntimeEnvironment();
183
- }
184
- }
185
- function persistRunEffects(summaryPath, artifactManifestPath, state) {
186
- for (const filePath of [summaryPath, artifactManifestPath]) {
187
- let document;
188
- try {
189
- document = JSON.parse(fs.readFileSync(filePath, "utf8"));
190
- } catch {
191
- document = null;
192
- }
193
- if (!isRecord(document)) {
194
- throw new Error(`Recipe evidence file is not valid JSON: ${filePath}`);
195
- }
196
- document.recovered = [...state.recovered];
197
- document.mutations = state.mutations.map((mutation) => ({ ...mutation }));
198
- fs.writeFileSync(filePath, `${JSON.stringify(document, null, 2)}
199
- `);
200
- }
201
- }
202
- async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
203
- const absoluteArtifactsDir = path.resolve(artifactsDir);
204
- const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
205
- const recordVideo = runtimeOptions.recordVideo ?? false;
206
- const { resolveRecipeTrustInput } = await importRecipeHarness();
207
- const trust = resolveRecipeTrustInput({
208
- sourceTrust: runtimeOptions.source?.trust,
209
- sourceKind: runtimeOptions.source?.kind,
210
- sourceName: runtimeOptions.source?.name,
211
- sourceDigest: runtimeOptions.source?.digest,
212
- approvalDigest: runtimeOptions.approval?.planDigest
213
- });
214
- const librarySources = executionLibrarySources(
215
- runtimeOptions.librarySources,
216
- trust.source?.trust ?? "trusted"
217
- );
218
- const { manifest, actionSources } = await resolveActionManifest(
219
- adapter,
220
- actionManifestPath,
221
- librarySources,
222
- process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR
223
- );
224
- const { createMetaMaskRunner } = await import("../runner.js");
225
- const recipeDocument = runtimeRecipeDocument(recipe, absoluteRecipePath);
226
- const trustedMutation = await loadTrustedMutationBase({
227
- fundingRequest: runtimeOptions.fundingRequest,
228
- fundingPreflight: runtimeOptions.fundingPreflight,
229
- fundingReleaseProof: runtimeOptions.fundingReleaseProof,
230
- fundingBalanceProof: runtimeOptions.fundingBalanceProof,
231
- artifactsDir: absoluteArtifactsDir,
232
- projectRoot,
233
- rootRecipe: recipeDocument
234
- });
235
- const surface = getAdapterSurface(adapter);
236
- const suppressAutoHud = surface.run?.autoHud?.() === false;
237
- const runnerOptions = {
238
- quietStdout: runtimeOptions.stdoutIsMachineContract === true,
239
- suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
240
- autoHud: suppressAutoHud ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
241
- onActionEvent: runtimeOptions.onActionEvent,
242
- actionSources,
243
- // A direct engineer invocation is the explicit trust boundary for a
244
- // task-local adapter. Inherited gateway provenance remains authoritative,
245
- // so an untrusted worker cannot promote itself with CLI flags.
246
- trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
247
- };
248
- let runner = await createMetaMaskRunner(adapter, manifest, {
249
- ...runnerOptions,
250
- trustedMutation
251
- });
252
- const useFramedExtensionRecording = surface.recording?.framed !== void 0 && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
253
- const runRequest = {
254
- ...recipeDocument !== void 0 ? { recipeDocument } : absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
255
- artifactsDir: absoluteArtifactsDir,
256
- projectRoot,
257
- inheritProcessEnv: false,
258
- env: {
259
- ...recipeProcessEnvironment(),
260
- ...recipeRunEnv(adapter, runtimeOptions),
261
- METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
262
- },
263
- recordVideo: useFramedExtensionRecording ? false : recordVideo,
264
- ...trust,
265
- ...librarySources ? { librarySources } : {},
266
- adapter,
267
- ...runtimeOptions.params ? { params: runtimeOptions.params } : {}
268
- };
269
- if (trustedMutation) {
270
- const executionPlan = await runner.preflight(runRequest);
271
- const authorizedMutation = await authorizeTrustedMutationPlan(trustedMutation, executionPlan);
272
- runner = await createMetaMaskRunner(adapter, manifest, {
273
- ...runnerOptions,
274
- trustedMutation: authorizedMutation
275
- });
276
- }
277
- const provenanceInput = {
278
- adapter,
279
- projectRoot,
280
- recipeDocument: recipeDocument ?? recipe,
281
- ...absoluteRecipePath ? { recipePath: absoluteRecipePath } : {},
282
- ...librarySources ? { librarySources } : {},
283
- excludedProductRoots: [
284
- absoluteArtifactsDir,
285
- recipeRuntimePath(projectRoot, "operations"),
286
- recipeRuntimePath(projectRoot, "lock-members")
287
- ],
288
- helperPaths: commandHelperPaths(recipeDocument ?? recipe, projectRoot)
289
- };
290
- const startProvenance = await captureExecutionProvenance(
291
- provenanceInput,
292
- "start"
293
- );
294
- return {
295
- runner,
296
- absoluteArtifactsDir,
297
- useFramedExtensionRecording,
298
- runRequest,
299
- provenanceInput,
300
- provenanceSnapshots: [startProvenance]
301
- };
302
- }
303
- function commandHelperPaths(recipe, projectRoot) {
304
- const helpers = /* @__PURE__ */ new Set();
305
- visitRecipeValues(recipe, (value) => {
306
- if (!isRecord(value) || value.action !== "command" || typeof value.cmd !== "string") return;
307
- const candidates = commandHelperTokens(value.cmd);
308
- for (const token of candidates) {
309
- if (/[$`{}]/u.test(token)) {
310
- throw new Error(`Command helper path cannot be bound before execution: ${token}`);
311
- }
312
- if (!isHelperToken(token)) continue;
313
- const absolute = path.isAbsolute(token) ? path.resolve(token) : path.resolve(projectRoot, token);
314
- let stat;
315
- try {
316
- stat = fs.lstatSync(absolute);
317
- } catch {
318
- throw new Error(`Command helper source does not exist: ${token}`);
319
- }
320
- if (stat.isSymbolicLink() || !stat.isFile()) {
321
- throw new Error(`Command helper source must be a regular file: ${token}`);
322
- }
323
- helpers.add(absolute);
324
- }
325
- });
326
- return [...helpers].sort();
327
- }
328
- function commandHelperTokens(command, depth = 0) {
329
- if (depth > 4) {
330
- throw new Error("Command helper sources exceed the supported shell nesting depth.");
331
- }
332
- const tokens = shellTokens(command);
333
- const candidates = new Set(tokens.filter(isHelperToken));
334
- for (const token of tokens) {
335
- const assignment = /^(?:--)?(?:config|configuration|manifest|project|recipe|schema|tsconfig)=([^=]+)$/iu.exec(
336
- token
337
- );
338
- const value = assignment?.[1];
339
- if (value && (isHelperToken(value) || isConfigInputToken(value))) {
340
- candidates.add(value);
341
- }
342
- }
343
- for (let index = 0; index < tokens.length - 1; index += 1) {
344
- if (!isScriptInterpreter(tokens[index])) continue;
345
- const option = tokens[index + 1];
346
- if (isShellInterpreter(tokens[index]) && /^-[a-z]*c[a-z]*$/iu.test(option) && tokens[index + 2]) {
347
- for (const nested of commandHelperTokens(tokens[index + 2], depth + 1)) {
348
- candidates.add(nested);
349
- }
350
- continue;
351
- }
352
- if (!option.startsWith("-")) candidates.add(option);
353
- }
354
- return candidates;
355
- }
356
- function visitRecipeValues(value, visit) {
357
- visit(value);
358
- if (Array.isArray(value)) {
359
- for (const child of value) visitRecipeValues(child, visit);
360
- return;
361
- }
362
- if (!isRecord(value)) return;
363
- for (const child of Object.values(value)) visitRecipeValues(child, visit);
364
- }
365
- function isHelperToken(token) {
366
- return !token.startsWith("-") && !token.includes("=") && !/^[a-z][a-z0-9+.-]*:\/\//iu.test(token) && /^\S+\.(?:cjs|mjs|js|jsx|ts|tsx|sh|py)$/u.test(token);
367
- }
368
- function isConfigInputToken(token) {
369
- return !token.startsWith("-") && !token.includes("=") && /^\S+\.(?:json|json5|ya?ml|toml)$/u.test(token);
370
- }
371
- function isScriptInterpreter(token) {
372
- return /^(?:node|bash|sh|python|python3)$/u.test(path.basename(token));
373
- }
374
- function isShellInterpreter(token) {
375
- return /^(?:bash|sh)$/u.test(path.basename(token));
376
- }
377
- function shellTokens(command) {
378
- const tokens = [];
379
- let token = "";
380
- let quote = null;
381
- let escaped = false;
382
- const push = () => {
383
- if (token) tokens.push(token);
384
- token = "";
385
- };
386
- for (const character of command) {
387
- if (escaped) {
388
- token += character;
389
- escaped = false;
390
- continue;
391
- }
392
- if (character === "\\" && quote !== "'") {
393
- escaped = true;
394
- continue;
395
- }
396
- if (quote) {
397
- if (character === quote) quote = null;
398
- else token += character;
399
- continue;
400
- }
401
- if (character === "'" || character === '"') {
402
- quote = character;
403
- continue;
404
- }
405
- if (/\s|[;&|<>]/u.test(character)) push();
406
- else token += character;
407
- }
408
- if (quote || escaped) {
409
- throw new Error("Command helper sources cannot be bound from an unterminated shell token.");
410
- }
411
- push();
412
- return tokens;
413
- }
414
- function runtimeRecipeDocument(recipe, absoluteRecipePath) {
415
- if (typeof recipe !== "string") return recipe;
416
- if (!absoluteRecipePath || !fs.existsSync(absoluteRecipePath)) return void 0;
417
- try {
418
- return JSON.parse(fs.readFileSync(absoluteRecipePath, "utf8"));
419
- } catch {
420
- return void 0;
421
- }
422
- }
423
- function recipeProcessEnvironment() {
424
- const env = { ...process.env };
425
- delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
426
- delete env.MM_HARNESS_OPERATION_ID;
427
- return env;
428
- }
429
- function executionLibrarySources(sources, invocationTrust) {
430
- return sources?.map((source) => {
431
- if (source.name === "metamask") return source;
432
- return {
433
- ...source,
434
- provenance: {
435
- ...source.provenance,
436
- kind: source.provenance?.kind ?? "library",
437
- trust: invocationTrust === "trusted" ? "trusted" : "unknown",
438
- name: source.provenance?.name ?? source.name ?? path.basename(source.root)
439
- }
440
- };
441
- });
442
- }
443
- const RUN_ENV_KEYS = [
444
- "CDP_PORT",
445
- "RECIPE_CDP_PORT",
446
- "WATCHER_PORT",
447
- "METRO_PORT",
448
- "METAMASK_RECIPE_EXTENSION_AUTOLAUNCH"
449
- ];
450
- function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
451
- const surface = getAdapterSurface(adapter);
452
- const previousEnv = Object.fromEntries(
453
- [...RUN_ENV_KEYS, ...surface.run?.envKeys ?? []].map((key) => [key, process.env[key]])
454
- );
455
- const pinnedEnv = surface.run?.pinnedEnv?.();
456
- surface.resolveSlotPorts(projectRoot);
457
- if (pinnedEnv) {
458
- for (const [key, value] of Object.entries(pinnedEnv)) {
459
- restoreEnv(key, value);
460
- }
461
- }
462
- if (runtimeOptions.cdpPort) {
463
- process.env.CDP_PORT = runtimeOptions.cdpPort;
464
- process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
465
- }
466
- if (runtimeOptions.watcherPort) {
467
- process.env.WATCHER_PORT = runtimeOptions.watcherPort;
468
- process.env.METRO_PORT = runtimeOptions.watcherPort;
469
- }
470
- if (runtimeOptions.launchExistingDist) {
471
- process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
472
- }
473
- surface.run?.activateEnv?.(projectRoot);
474
- return () => {
475
- for (const [key, value] of Object.entries(previousEnv)) restoreEnv(key, value);
476
- };
477
- }
478
- function actionSourceMap(sources) {
479
- const roots = {};
480
- for (const [action, source] of sources) {
481
- if (source.tier === "official") continue;
482
- roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(
483
- path.dirname(path.dirname(source.manifestPath)),
484
- "actions"
485
- ));
486
- }
487
- return JSON.stringify(roots);
488
- }
489
- function recipeRunEnv(adapter, runtimeOptions = {}) {
490
- const base = {
491
- CDP_PORT: runtimeOptions.cdpPort ?? process.env.CDP_PORT,
492
- RECIPE_CDP_PORT: runtimeOptions.cdpPort ?? process.env.RECIPE_CDP_PORT,
493
- FARMSLOT_SLOT_ID: runtimeOptions.slot ?? process.env.FARMSLOT_SLOT_ID,
494
- SLOT_ID: runtimeOptions.slot ?? process.env.SLOT_ID,
495
- PLATFORM: process.env.PLATFORM,
496
- MM_HARNESS_EXPLICIT_PLATFORM: process.env.MM_HARNESS_EXPLICIT_PLATFORM
497
- };
498
- const childEnv = getAdapterSurface(adapter).run?.childEnv;
499
- return childEnv ? childEnv(base) : base;
500
- }
501
- async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
502
- await getAdapterSurface(adapter).run?.prepareRuntime?.(projectRoot, runtimeOptions);
503
- }
504
- function restoreEnv(key, value) {
505
- if (value === void 0) delete process.env[key];
506
- else process.env[key] = value;
507
- }
508
- async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySources, rootRef, params, proof = false) {
509
- const protocol = await importRecipeProtocol();
510
- let externalRecipeIds;
511
- let libraryResolution;
512
- const harness = await importRecipeHarness();
513
- if (librarySources && librarySources.length > 0) {
514
- libraryResolution = await harness.loadRecipeLibraries(librarySources, {
515
- adapter
516
- });
517
- externalRecipeIds = new Set(libraryResolution.recipes.keys());
518
- }
519
- const validationOptions = externalRecipeIds !== void 0 ? { externalRecipeIds } : void 0;
520
- const withManifest = protocol.validateRecipeWithManifest(
521
- recipe,
522
- manifest,
523
- validationOptions
524
- );
525
- const findings = [
526
- ...withManifest.findings,
527
- ...withManifest.status === "valid" && params ? protocol.validateRecipeWithManifest(
528
- resolveMetaMaskParameterValue(recipe, params),
529
- manifest,
530
- validationOptions
531
- ).findings : [],
532
- ...validateMetaMaskActionInputs(recipe, adapter, params)
533
- ];
534
- if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
535
- try {
536
- const digest = protocol.digestRecipeDocument(recipe);
537
- const dependencies = harness.resolveRecipeDependencies({
538
- rootRef: rootRef ?? `$root:${digest}`,
539
- root: recipe,
540
- rootSource: {
541
- kind: "recipe-file",
542
- trust: "unknown",
543
- name: rootRef ?? "recipe file",
544
- digest
545
- },
546
- recipes: libraryResolution.recipes
547
- });
548
- if (params) {
549
- harness.validateRecipeDependencyParams({
550
- root: recipe,
551
- params,
552
- recipes: dependencies.recipes
553
- });
554
- }
555
- for (const dependency of dependencies.recipes.values()) {
556
- findings.push(
557
- ...protocol.validateRecipeWithManifest(
558
- dependency.document,
559
- manifest,
560
- validationOptions
561
- ).findings
562
- );
563
- }
564
- const validateDependencyInstances = (document, parentParams) => {
565
- if (!isRecord(document) || !isRecord(document.workflow) || !isRecord(document.workflow.nodes)) return;
566
- for (const rawNode of Object.values(document.workflow.nodes)) {
567
- const node = resolveMetaMaskParameterValue(rawNode, parentParams);
568
- if (!isRecord(node) || node.action !== "call" || typeof node.ref !== "string") continue;
569
- const dependency = dependencies.recipes.get(protocol.normalizeRecipeRef(node.ref));
570
- if (!dependency) continue;
571
- const supplied = isRecord(node.params) ? node.params : {};
572
- const childParams = protocol.applyRecipeParamDefaults(
573
- supplied,
574
- isRecord(dependency.document) ? dependency.document.paramsSchema : void 0
575
- );
576
- findings.push(...protocol.validateRecipeWithManifest(
577
- resolveMetaMaskParameterValue(dependency.document, childParams),
578
- manifest,
579
- validationOptions
580
- ).findings);
581
- findings.push(...validateMetaMaskActionInputs(dependency.document, adapter, childParams));
582
- validateDependencyInstances(dependency.document, childParams);
583
- }
584
- };
585
- validateDependencyInstances(recipe, params ?? {});
586
- if (proof && !findings.some((finding) => finding.severity === "error")) {
587
- findings.push(...validateRuntimeProofPlan(
588
- recipe,
589
- new Map([...dependencies.recipes].map(([ref, dependency]) => [ref, dependency.document]))
590
- ));
591
- }
592
- } catch (error) {
593
- const resolutionError = error instanceof harness.RecipeResolutionError ? error : void 0;
594
- if (!resolutionError) throw error;
595
- findings.push({
596
- severity: "error",
597
- code: resolutionError.code,
598
- path: "workflow",
599
- message: `${resolutionError.message} Next: ${resolutionError.userAction}`
600
- });
601
- }
602
- }
603
- const errors = findings.filter(
604
- (finding) => finding.severity === "error"
605
- ).length;
606
- const warnings = findings.length - errors;
607
- return {
608
- status: errors > 0 ? "invalid" : "valid",
609
- findings,
610
- summary: { errors, warnings }
611
- };
612
- }
613
- function isRecipeFile(p) {
614
- try {
615
- return fs.statSync(p).isFile();
616
- } catch {
617
- return false;
618
- }
619
- }
620
- async function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
621
- const direct = path.resolve(recipeArg);
622
- if (isRecipeFile(direct)) return { recipeFile: direct };
623
- const sources = effectiveLibrarySources(librarySources);
624
- const harness = await importRecipeHarness();
625
- const resolution = await harness.loadRecipeLibraries(sources, { adapter });
626
- if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
627
- const selected = resolution.recipes.get(recipeArg);
628
- if (selected) return { recipeFile: selected.path, ref: selected.ref };
629
- }
630
- const available = [...resolution.recipes.keys()].sort();
631
- const nonCanonical = sources.filter((source) => source.name !== "metamask");
632
- const notFoundCore = nonCanonical.length > 0 ? `recipe not found: ${recipeArg} \u2014 not a file, and no recipe matched in library sources [${sources.map((source) => source.name ?? path.basename(source.root)).join(", ")}].` : `recipe not found: ${recipeArg} \u2014 not a file, and no packaged library recipe matched.`;
633
- const suffix = available.length > 0 ? ` Library recipes for ${adapter}: ${available.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
634
- return { notFound: notFoundCore + suffix };
635
- }
636
- function effectiveLibrarySources(librarySources) {
637
- return librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: path.resolve(runnerDir, "library") }];
638
- }
639
- async function listRunnableRecipes(adapter, librarySources) {
640
- return (await runnableLibraryRecipes(adapter, effectiveLibrarySources(librarySources))).map(
641
- (recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
642
- ).sort((left, right) => left.name.localeCompare(right.name));
643
- }
644
- async function describeRunnableRecipe(recipeArg, adapter, librarySources) {
645
- const resolved = await resolveRunRecipeArg(
646
- recipeArg,
647
- adapter,
648
- librarySources
649
- );
650
- if ("notFound" in resolved) return resolved;
651
- let document;
652
- try {
653
- document = JSON.parse(fs.readFileSync(resolved.recipeFile, "utf8"));
654
- } catch (error) {
655
- return {
656
- unreadable: `could not read recipe ${resolved.recipeFile}: ${error instanceof Error ? error.message : String(error)}`
657
- };
658
- }
659
- if (!isRecord(document)) {
660
- return {
661
- unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.`
662
- };
663
- }
664
- const harness = await importRecipeHarness();
665
- const libraryResolution = await harness.loadRecipeLibraries(
666
- effectiveLibrarySources(librarySources),
667
- { adapter }
668
- );
669
- const selected = resolved.ref ? libraryResolution.recipes.get(resolved.ref) : void 0;
670
- const composition = recipeComposition(document, libraryResolution.recipes);
671
- const summary = recipeSummary(
672
- resolved.ref ?? recipeArg,
673
- adapter,
674
- document,
675
- selected
676
- );
677
- return {
678
- recipe: {
679
- ...summary,
680
- source: selected?.source ?? "file",
681
- file: selected?.file ?? resolved.recipeFile,
682
- path: resolved.recipeFile,
683
- ...typeof document.$schema === "string" ? { $schema: document.$schema } : {},
684
- actions: composition.actions,
685
- nestedRecipes: composition.nestedRecipes,
686
- unresolvedRecipes: composition.unresolvedRecipes
687
- }
688
- };
689
- }
690
- function recipeSummary(ref, adapter, document, selected) {
691
- return {
692
- name: ref,
693
- source: selected?.source ?? "file",
694
- file: selected?.file ?? "",
695
- shadows: selected?.shadows ?? [],
696
- adapter,
697
- variant: selected?.adapter ?? null,
698
- parameters: recipeParameters(document),
699
- ...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
700
- ...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {}
701
- };
702
- }
703
- function recipeParameters(document) {
704
- if (!isRecord(document.paramsSchema)) return [];
705
- const schema = document.paramsSchema;
706
- const required = new Set(
707
- Array.isArray(schema.required) ? schema.required.filter(
708
- (entry) => typeof entry === "string"
709
- ) : []
710
- );
711
- if (!isRecord(schema.properties)) return [];
712
- return Object.entries(schema.properties).map(([name, raw]) => {
713
- const property = isRecord(raw) ? raw : {};
714
- return {
715
- name,
716
- ...typeof property.type === "string" ? { type: property.type } : {},
717
- required: required.has(name),
718
- ...Object.hasOwn(property, "default") ? { default: property.default } : {},
719
- ...typeof property.description === "string" ? { description: property.description } : {},
720
- ...Array.isArray(property.enum) ? { enum: property.enum } : {}
721
- };
722
- });
723
- }
724
- async function validateRunRecipeStatic(recipeArg, adapter, options, params = {}) {
725
- let librarySources = await resolveMetaMaskLibrarySources(
726
- optionStrings(options, "library")
727
- );
728
- const resolved = await resolveRunRecipeArg(
729
- recipeArg,
730
- adapter,
731
- librarySources
732
- );
733
- const recipeFile = "recipeFile" in resolved ? resolved.recipeFile : path.resolve(recipeArg);
734
- if ("recipeFile" in resolved && !resolved.ref) {
735
- librarySources = await resolveMetaMaskLibrarySources(
736
- optionStrings(options, "library"),
737
- recipeFile
738
- );
739
- }
740
- const empty = {
741
- recipe: void 0,
742
- recipeFile,
743
- findings: [],
744
- errorCount: 0,
745
- manifestOk: false,
746
- schemaValid: false,
747
- effectiveParams: params,
748
- ...librarySources ? { librarySources } : {}
749
- };
750
- if ("notFound" in resolved) {
751
- return {
752
- ...empty,
753
- usageError: {
754
- code: "RECIPE_NOT_FOUND",
755
- message: resolved.notFound + await actionInsteadOfRecipeHint(recipeArg, adapter, options)
756
- }
757
- };
758
- }
759
- let recipe;
760
- try {
761
- recipe = JSON.parse(fs.readFileSync(recipeFile, "utf8"));
762
- } catch (error) {
763
- return {
764
- ...empty,
765
- usageError: {
766
- code: "RECIPE_UNPARSEABLE",
767
- message: `recipe is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
768
- }
769
- };
770
- }
771
- const findings = [];
772
- const { manifest } = await resolveActionManifest(
773
- adapter,
774
- optionString(options, "actionManifest"),
775
- librarySources
776
- );
777
- let manifestOk = true;
778
- try {
779
- await validateManifest(manifest);
780
- } catch (error) {
781
- manifestOk = false;
782
- findings.push({
783
- severity: "error",
784
- code: "manifest.invalid",
785
- path: actionManifestPathOption(options, adapter),
786
- message: error instanceof Error ? error.message : String(error)
787
- });
788
- }
789
- let effectiveParams = params;
790
- if (isRecord(recipe)) {
791
- const protocol = await importRecipeProtocol();
792
- effectiveParams = protocol.applyRecipeParamDefaults(
793
- params,
794
- recipe.paramsSchema
795
- );
796
- findings.push(
797
- ...protocol.validateRecipeParams(effectiveParams, recipe.paramsSchema).findings
798
- );
799
- }
800
- const validation = manifestOk ? await validateRecipeAdapterAware(
801
- adapter,
802
- recipe,
803
- manifest,
804
- librarySources,
805
- resolved.ref,
806
- effectiveParams,
807
- optionFlag(options, "proof")
808
- ) : {
809
- status: "invalid",
810
- findings: [],
811
- summary: { errors: 1, warnings: 0 }
812
- };
813
- findings.push(...validation.findings);
814
- const errorCount = findings.filter(
815
- (finding) => finding.severity === "error"
816
- ).length;
817
- return {
818
- recipe,
819
- recipeFile,
820
- findings,
821
- errorCount,
822
- manifestOk,
823
- schemaValid: validation.status === "valid" && errorCount === 0,
824
- effectiveParams,
825
- ...librarySources ? { librarySources } : {}
826
- };
827
- }
828
- async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
829
- if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
830
- try {
831
- const librarySources = await resolveMetaMaskLibrarySources(
832
- optionStrings(options, "library")
833
- );
834
- const { manifest } = await resolveActionManifest(
835
- adapter,
836
- optionString(options, "actionManifest"),
837
- librarySources
838
- );
839
- const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
840
- const actions = getRecipeActionManifestActionNames(manifest);
841
- const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
842
- if (matches.length !== 1) return "";
843
- const device = optionString(options, "device");
844
- const actionManifest = optionString(options, "actionManifest");
845
- const parts = ["mm-harness", "call", matches[0]];
846
- parts.push(
847
- "--adapter",
848
- adapter,
849
- "--target",
850
- shellQuoteArg(targetPath(options))
851
- );
852
- if (device) parts.push("--device", shellQuoteArg(device));
853
- if (actionManifest)
854
- parts.push("--action-manifest", shellQuoteArg(actionManifest));
855
- return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
856
- } catch {
857
- return "";
858
- }
859
- }
860
- async function resolveMetaMaskLibrarySources(libraryEntry, recipePath) {
861
- const harness = await importRecipeHarness();
862
- const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
863
- const sources = await harness.resolveRecipeLibrarySources({
864
- ...cliEntries.length > 0 ? { cliEntries } : {},
865
- ...recipePath ? { recipePath } : {}
866
- });
867
- const canonicalRoot = path.resolve(runnerDir, "library");
868
- const configuredCanonical = sources.find(
869
- (source) => source.name === "metamask"
870
- );
871
- if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
872
- throw usageError(
873
- `Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
874
- );
875
- }
876
- if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
877
- sources.push({ name: "metamask", root: canonicalRoot });
878
- }
879
- return Promise.all(
880
- sources.map(async (source) => {
881
- const isBundled = source.name === "metamask";
882
- const detected = isBundled ? {} : await gitLibraryProvenance(source.root);
883
- return {
884
- ...source,
885
- provenance: {
886
- kind: isBundled ? "bundled" : "library",
887
- trust: isBundled ? "trusted" : "unknown",
888
- name: source.name ?? path.basename(source.root),
889
- ...detected,
890
- ...source.provenance
891
- }
892
- };
893
- })
894
- );
895
- }
896
- function synthesizeOneNodeRecipe(action, args) {
897
- return {
898
- $schema: "https://farmslot.io/schemas/recipe-v1.schema.json",
899
- title: `mm-harness call ${action}`,
900
- description: `Ad-hoc single-action execution of ${action} via the real engine path (mm-harness call).`,
901
- workflow: {
902
- entry: "call",
903
- nodes: {
904
- call: {
905
- ...args,
906
- action,
907
- next: "done",
908
- intent: typeof args.intent === "string" ? args.intent : `Complete the requested ${action} operation`
909
- },
910
- done: { action: "end", status: "pass" }
911
- }
912
- }
913
- };
914
- }
915
- async function runOneNode(adapter, action, args, target, actionManifest) {
916
- const librarySources = await resolveMetaMaskLibrarySources(void 0);
917
- const { manifest } = await resolveActionManifest(
918
- adapter,
919
- actionManifest,
920
- librarySources
921
- );
922
- const recipe = synthesizeOneNodeRecipe(action, args);
923
- const validation = await validateRecipeAdapterAware(
924
- adapter,
925
- recipe,
926
- manifest,
927
- librarySources
928
- );
929
- if (validation.status === "invalid") return { status: "fail" };
930
- const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-fixtures-"));
931
- const recipeFile = path.join(scratch, "set.recipe.json");
932
- fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
933
- `);
934
- const result = await runRecipe(
935
- adapter,
936
- recipeFile,
937
- path.join(scratch, "artifacts"),
938
- target,
939
- actionManifest,
940
- {
941
- librarySources,
942
- stdoutIsMachineContract: true
943
- }
944
- );
945
- return { status: result.status === "pass" ? "pass" : "fail" };
946
- }
947
- async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
948
- if (recipeRunning(target)) {
949
- const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
950
- const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
951
- if (json) {
952
- console.log(
953
- JSON.stringify(
954
- {
955
- schemaVersion: 1,
956
- status: "fail",
957
- recoverable: false,
958
- error: { code: "RECIPE_RUNNING", message: msg, userAction }
959
- },
960
- null,
961
- 2
962
- )
963
- );
964
- } else {
965
- console.error(`\u2717 mm-harness: ${msg}
966
- Next: ${userAction}`);
967
- }
968
- return EXIT.bounded;
969
- }
970
- const healValue = optionString(options, "heal");
971
- const healOpts = {};
972
- if (healValue !== void 0) healOpts.heal = healValue;
973
- const heal = parseHeal(healOpts, "infra-only");
974
- if (typeof heal !== "string") {
975
- console.error(heal.error);
976
- return EXIT.usage;
977
- }
978
- const state = newHealState();
979
- const runtimeCheck = getAdapterSurface(adapter).run?.runtimeCheck?.({
980
- target,
981
- options,
982
- heal,
983
- state,
984
- json,
985
- appRestartAuthored: prepareOptions.appRestartAuthored,
986
- onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
987
- });
988
- prepareOptions.onPhase?.("install");
989
- const ensured = await ensureOverlay(
990
- adapter,
991
- target,
992
- heal,
993
- state,
994
- json
995
- );
996
- if (!ensured.ok) {
997
- const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
998
- if (json) {
999
- console.log(
1000
- JSON.stringify(
1001
- {
1002
- schemaVersion: 1,
1003
- status: "fail",
1004
- recoverable: false,
1005
- mutations: state.mutations,
1006
- error: {
1007
- code: "OVERLAY_INSTALL_FAILED",
1008
- message: ensured.error,
1009
- userAction
1010
- }
1011
- },
1012
- null,
1013
- 2
1014
- )
1015
- );
1016
- } else {
1017
- console.error(
1018
- `\u2717 overlay auto-ensure failed: ${ensured.error}
1019
- Next: ${userAction}`
1020
- );
1021
- }
1022
- return EXIT.infra;
1023
- }
1024
- prepareOptions.onPhase?.("healthcheck");
1025
- if (runtimeCheck) {
1026
- const current = await runtimeCheck();
1027
- if (current !== null) return current;
1028
- }
1029
- return { state, heal };
1030
- }
1031
- async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
1032
- const source = await import("../adapters/mobile/source-freshness.js");
1033
- const check = deps.check ?? (() => source.mobileSourceCheck(target));
1034
- const initial = check();
1035
- if (initial.status === "unavailable") {
1036
- if (json) {
1037
- console.log(
1038
- JSON.stringify(
1039
- {
1040
- schemaVersion: 1,
1041
- status: "fail",
1042
- recoverable: false,
1043
- error: initial.error
1044
- },
1045
- null,
1046
- 2
1047
- )
1048
- );
1049
- } else {
1050
- console.error(`\u2717 ${initial.error.message}`);
1051
- console.error(` Next: ${initial.error.userAction}`);
1052
- }
1053
- return EXIT.usage;
1054
- }
1055
- if (initial.status === "current") return null;
1056
- const targetArg = JSON.stringify(path.resolve(target));
1057
- const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
1058
- const message = `Mobile loaded source is not current (${initial.status}).`;
1059
- if (json) {
1060
- console.log(
1061
- JSON.stringify(
1062
- {
1063
- schemaVersion: 1,
1064
- status: "fail",
1065
- recoverable: true,
1066
- mutations: state.mutations,
1067
- error: { code: "MOBILE_SOURCE_NOT_LOADED", message, userAction }
1068
- },
1069
- null,
1070
- 2
1071
- )
1072
- );
1073
- } else {
1074
- console.error(`\u2717 ${message}`);
1075
- console.error(` Next: ${userAction}`);
1076
- }
1077
- return EXIT.infra;
1078
- }
1079
- async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
1080
- const decide = deps.decide ?? (async () => {
1081
- const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
1082
- const port = Number(process.env.CDP_PORT);
1083
- return decideExtensionReadiness(target, {
1084
- ...Number.isInteger(port) && port > 0 ? { cdpPort: port } : {},
1085
- pageMode: "home-or-sidepanel"
1086
- });
1087
- });
1088
- const initial = await decide();
1089
- if (initial.decision === "ready") return null;
1090
- if (initial.reasonCode === "release-artifact-identity-invalid" || initial.reasonCode === "release-artifact-runtime-missing" || initial.reasonCode === "release-artifact-runtime-stale") {
1091
- const message = initial.reasons[0] ?? "The loaded release artifact identity is invalid.";
1092
- const userAction2 = "rerun runtime-launch with the same artifact source";
1093
- if (json) {
1094
- console.log(JSON.stringify({
1095
- schemaVersion: 1,
1096
- status: "fail",
1097
- recoverable: false,
1098
- error: { code: "RELEASE_ARTIFACT_NOT_CURRENT", message, userAction: userAction2 }
1099
- }, null, 2));
1100
- } else {
1101
- console.error(`\u2717 ${message}`);
1102
- console.error(` Next: ${userAction2}`);
1103
- }
1104
- return EXIT.infra;
1105
- }
1106
- if (initial.reasonCode === "runtime-dist-check-failed") {
1107
- const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
1108
- const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
1109
- if (json) {
1110
- console.log(
1111
- JSON.stringify(
1112
- {
1113
- schemaVersion: 1,
1114
- status: "fail",
1115
- recoverable: false,
1116
- error: {
1117
- code: "EXTENSION_RUNTIME_CHECK_FAILED",
1118
- message,
1119
- userAction: userAction2
1120
- }
1121
- },
1122
- null,
1123
- 2
1124
- )
1125
- );
1126
- } else {
1127
- console.error(`\u2717 ${message}`);
1128
- console.error(` Next: ${userAction2}`);
1129
- }
1130
- return EXIT.infra;
1131
- }
1132
- if (![
1133
- "dist-stale",
1134
- "dist-missing",
1135
- "runtime-dist-stale",
1136
- "runtime-dist-missing"
1137
- ].includes(initial.reasonCode)) {
1138
- return null;
1139
- }
1140
- const tier = initial.decision === "build" ? "build" : "quick";
1141
- const canHeal = heal === "auto" || heal === "infra-only" && tier === "quick";
1142
- const targetArg = JSON.stringify(path.resolve(target));
1143
- const userAction = tier === "build" ? `mm-harness launch --build --verify --adapter extension --target ${targetArg}` : `mm-harness launch --verify --adapter extension --target ${targetArg}`;
1144
- if (!canHeal) {
1145
- const message = `Extension proof runtime is not current (${initial.reasonCode}): ${initial.reasons[0] ?? initial.decision}`;
1146
- if (json) {
1147
- console.log(
1148
- JSON.stringify(
1149
- {
1150
- schemaVersion: 1,
1151
- status: "fail",
1152
- recoverable: true,
1153
- mutations: state.mutations,
1154
- error: {
1155
- code: "EXTENSION_RUNTIME_NOT_CURRENT",
1156
- message,
1157
- userAction
1158
- }
1159
- },
1160
- null,
1161
- 2
1162
- )
1163
- );
1164
- } else {
1165
- console.error(`\u2717 ${message}`);
1166
- console.error(` Next: ${userAction}`);
1167
- }
1168
- return EXIT.infra;
1169
- }
1170
- const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
1171
- const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
1172
- if (productConfigBlock) {
1173
- const message = productConfigBlock.message;
1174
- const userAction2 = productConfigBlock.userAction;
1175
- if (json) {
1176
- console.log(
1177
- JSON.stringify(
1178
- {
1179
- schemaVersion: 1,
1180
- status: "fail",
1181
- recoverable: false,
1182
- mutations: state.mutations,
1183
- error: {
1184
- code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
1185
- message,
1186
- userAction: userAction2
1187
- }
1188
- },
1189
- null,
1190
- 2
1191
- )
1192
- );
1193
- } else {
1194
- console.error(`\u2717 ${message}`);
1195
- console.error(` Next: ${userAction2}`);
1196
- }
1197
- return EXIT.infra;
1198
- }
1199
- state.attemptedRecoveries.push(recoveryCode);
1200
- deps.onRecovery?.(recoveryCode);
1201
- if (!json)
1202
- console.error(
1203
- `\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`
1204
- );
1205
- const launch = deps.launch ?? (async (requestedTier) => {
1206
- const { launchExtension } = await import("./launch/extension.js");
1207
- const { readExtensionPinnedFlags } = await import("../adapters/extension/feature-flags.js");
1208
- const { fixtureFeatureFlags, remoteFlagRequestFor } = await import("../feature-flags.js");
1209
- const { walletFixturePath } = await import("../paths.js");
1210
- return launchExtension(
1211
- target,
1212
- requestedTier,
1213
- false,
1214
- "fullscreen",
1215
- remoteFlagRequestFor(readExtensionPinnedFlags(target).pinned ?? fixtureFeatureFlags(walletFixturePath(target)))
1216
- );
1217
- });
1218
- const result = await launch(tier);
1219
- const verified = result.status === 0 ? await decide() : initial;
1220
- if (result.status !== 0 || verified.decision !== "ready") {
1221
- const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
1222
- if (json) {
1223
- console.log(
1224
- JSON.stringify(
1225
- {
1226
- schemaVersion: 1,
1227
- status: "fail",
1228
- recoverable: false,
1229
- attemptedRecoveries: state.attemptedRecoveries,
1230
- error: {
1231
- code: "EXTENSION_RUNTIME_RECOVERY_FAILED",
1232
- message,
1233
- userAction,
1234
- originalError: result.output
1235
- }
1236
- },
1237
- null,
1238
- 2
1239
- )
1240
- );
1241
- } else {
1242
- console.error(`\u2717 ${message}`);
1243
- console.error(` Next: ${userAction}`);
1244
- }
1245
- return EXIT.infra;
1246
- }
1247
- state.recovered.push(recoveryCode);
1248
- state.mutations.push({
1249
- type: "runtime",
1250
- action: tier === "build" ? "rebuilt" : "reloaded",
1251
- reasonCode: initial.reasonCode
1252
- });
1253
- return null;
1254
- }
1255
- function readRunFailureText(result) {
1256
- try {
1257
- const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
1258
- const entries = Array.isArray(trace.entries) ? trace.entries : [];
1259
- return entries.filter(
1260
- (entry) => entry && entry.ok === false && typeof entry.error === "string"
1261
- ).map((entry) => entry.error).join("\n").trim();
1262
- } catch {
1263
- return "";
1264
- }
1265
- }
1266
- async function executeWithHealBounds(exec, target, state) {
1267
- const result = await exec();
1268
- if (result.status === "pass" || result.status === "unknown") return { result, violation: null };
1269
- return {
1270
- result,
1271
- violation: checkHealBounds(target, readRunFailureText(result), state)
1272
- };
1273
- }
1274
- function emitHealViolation(json, command, result, violation, state, adapter) {
1275
- const userAction = (adapter ? getAdapterSurface(adapter).run?.violationUserAction?.(violation) : void 0) ?? violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
1276
- if (json) {
1277
- console.log(
1278
- JSON.stringify(
1279
- {
1280
- schemaVersion: 1,
1281
- command,
1282
- status: "fail",
1283
- recoverable: false,
1284
- recovered: state.recovered,
1285
- mutations: state.mutations,
1286
- attemptedRecoveries: state.attemptedRecoveries,
1287
- summaryPath: result.summaryPath,
1288
- tracePath: result.tracePath,
1289
- artifactManifestPath: result.artifactManifestPath,
1290
- exitCode: violation.exitCode,
1291
- error: {
1292
- code: violation.code,
1293
- message: violation.message,
1294
- retryable: false,
1295
- userAction,
1296
- originalError: violation.originalError ?? null
1297
- }
1298
- },
1299
- null,
1300
- 2
1301
- )
1302
- );
1303
- } else {
1304
- const message = violation.code === "APP_LOGIC_FAILURE" && violation.originalError ? conciseFailureForHuman(violation.originalError) : violation.message;
1305
- console.error(
1306
- `\u2717 mm-harness ${command}: ${message}` + (violation.originalError && violation.code !== "APP_LOGIC_FAILURE" ? `
1307
- --- original failure ---
1308
- ${violation.originalError}` : "") + (userAction ? `
1309
- Next: ${userAction}` : "")
1310
- );
1311
- }
1312
- return violation.exitCode;
1313
- }
1314
- function countRecipeNodes(recipe) {
1315
- if (!isRecord(recipe)) return void 0;
1316
- const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
1317
- const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
1318
- return nodes ? Object.keys(nodes).length : void 0;
1319
- }
1320
- export {
1321
- activateRecipeRuntimeEnvironment,
1322
- countRecipeNodes,
1323
- describeRunnableRecipe,
1324
- emitHealViolation,
1325
- ensureExtensionProofRuntime,
1326
- ensureMobileProofRuntime,
1327
- executeWithHealBounds,
1328
- listRunnableRecipes,
1329
- persistRunEffects,
1330
- preflightRecipe,
1331
- prepareHeal,
1332
- prepareRuntimeIfNeeded,
1333
- resolveMetaMaskLibrarySources,
1334
- resolveRunRecipeArg,
1335
- runOneNode,
1336
- runRecipe,
1337
- synthesizeOneNodeRecipe,
1338
- validateRecipeAdapterAware,
1339
- validateRunRecipeStatic
1340
- };