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