@deeeed/metamask-harness 0.17.4 → 0.18.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 (93) hide show
  1. package/CHANGELOG.md +775 -0
  2. package/README.md +65 -71
  3. package/adapters/extension/ensure-browser.sh +12 -1
  4. package/adapters/extension/inject.mjs +7 -0
  5. package/adapters/extension/launch-browser.cjs +10 -3
  6. package/adapters/extension/lib/chrome-args.cjs +31 -0
  7. package/adapters/extension/lib/macos-focus.cjs +32 -0
  8. package/adapters/extension/live.sh +10 -20
  9. package/adapters/manifest.json +8 -0
  10. package/adapters/mobile/open-device.sh +45 -7
  11. package/adapters/mobile/verify.sh +15 -3
  12. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  13. package/adapters/shared/install-repo-deps.sh +1 -5
  14. package/adapters/shared/open-debug.mjs +184 -103
  15. package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
  16. package/adapters/shared/resolve-slot-ports.sh +22 -5
  17. package/bin/mm-harness +15 -3
  18. package/dist/adapters/core/surface.js +4 -1
  19. package/dist/adapters/extension/console-capture.js +3 -1
  20. package/dist/adapters/extension/harness-freshness.js +39 -0
  21. package/dist/adapters/extension/product-config.js +110 -0
  22. package/dist/adapters/extension/runtime-decision.js +20 -71
  23. package/dist/adapters/extension/surface.js +19 -1
  24. package/dist/adapters/mobile/prepare.js +17 -0
  25. package/dist/adapters/mobile/source-freshness.js +26 -41
  26. package/dist/adapters/mobile/surface.js +4 -1
  27. package/dist/adapters/resolve-slot-ports.js +2 -0
  28. package/dist/adapters/slot-ports.js +13 -32
  29. package/dist/adapters.js +50 -17
  30. package/dist/checkout-lock.js +27 -2
  31. package/dist/cli-color.js +19 -0
  32. package/dist/cli-commands.js +1 -1
  33. package/dist/cli.js +2 -3
  34. package/dist/command-contract.js +13 -3
  35. package/dist/commands/call.js +115 -29
  36. package/dist/commands/checklist.js +4 -1
  37. package/dist/commands/completion-candidates.js +20 -13
  38. package/dist/commands/debug.js +31 -38
  39. package/dist/commands/doctor.js +33 -6
  40. package/dist/commands/fixtures.js +65 -17
  41. package/dist/commands/flows.js +39 -10
  42. package/dist/commands/launch/extension.js +40 -15
  43. package/dist/commands/launch/index.js +41 -5
  44. package/dist/commands/list-executables.js +151 -29
  45. package/dist/commands/manifest.js +127 -18
  46. package/dist/commands/parse-args.js +11 -1
  47. package/dist/commands/run-engine.js +384 -56
  48. package/dist/commands/run.js +112 -17
  49. package/dist/commands/shared.js +22 -2
  50. package/dist/commands/status-probe.js +3 -0
  51. package/dist/commands/status.js +1 -0
  52. package/dist/completions-cache.js +1 -1
  53. package/dist/doctor.js +56 -6
  54. package/dist/harness.js +6 -5
  55. package/dist/heal-bounds.js +1 -1
  56. package/dist/live-adapter-contract.js +132 -12
  57. package/dist/manifest.js +161 -1
  58. package/dist/mm-harness-cli.js +13 -7
  59. package/dist/recipe-security.js +178 -0
  60. package/dist/runner.js +87 -13
  61. package/dist/runtime-context.js +8 -26
  62. package/docs/CONTRIBUTING.md +137 -0
  63. package/docs/QA.md +185 -0
  64. package/docs/RECIPES.md +161 -0
  65. package/docs/SECURITY.md +88 -0
  66. package/library/actions/core/perps/read_account.mjs +2 -2
  67. package/library/actions/core/perps/read_orders.mjs +2 -1
  68. package/library/actions/core/perps/read_positions.mjs +2 -1
  69. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  70. package/library/actions/extension/platform/cdp.mjs +1 -0
  71. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  72. package/library/actions/mobile/platform/bridge.mjs +1 -5
  73. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  74. package/library/manifests/core.action-manifest.json +61 -0
  75. package/library/manifests/extension.action-manifest.json +53 -0
  76. package/library/manifests/mobile.action-manifest.json +53 -0
  77. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  78. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  79. package/package.json +10 -6
  80. package/scripts/completions.sh +7 -7
  81. package/docs/ADAPTER-SURFACE.md +0 -26
  82. package/docs/CHEATSHEET.md +0 -48
  83. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  84. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  85. package/docs/CODE-MAP.md +0 -62
  86. package/docs/UX-PRINCIPLES.md +0 -66
  87. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  88. package/docs/architecture.md +0 -88
  89. package/docs/live-adapter-contract.md +0 -190
  90. package/docs/package-boundaries.md +0 -47
  91. package/docs/perps-flow-catalog.md +0 -235
  92. package/docs/recipe-libraries.md +0 -71
  93. package/docs/runtime-file-conventions.md +0 -36
@@ -9,8 +9,9 @@ import {
9
9
  recipeRunning,
10
10
  checkHealBounds
11
11
  } from "../heal-bounds.js";
12
- import { loadActionManifest, validateManifest } from "../manifest.js";
12
+ import { resolveActionManifest, validateManifest } from "../manifest.js";
13
13
  import { getAdapterSurface } from "../adapters/surface.js";
14
+ import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
14
15
  import {
15
16
  importRecipeHarness,
16
17
  importRecipeProtocol,
@@ -24,7 +25,7 @@ import {
24
25
  beginRunDiagnostics,
25
26
  finishRunDiagnostics
26
27
  } from "../run-diagnostics.js";
27
- import { EXIT } from "./shared.js";
28
+ import { EXIT, spawnScriptStreaming } from "./shared.js";
28
29
  import {
29
30
  actionManifestPathOption,
30
31
  optionString,
@@ -33,50 +34,38 @@ import {
33
34
  targetPath,
34
35
  usageError
35
36
  } from "./parse-args.js";
36
- async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
37
- const previousCdpPort = process.env.CDP_PORT;
38
- const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
39
- const previousWatcherPort = process.env.WATCHER_PORT;
40
- const previousMetroPort = process.env.METRO_PORT;
41
- const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
42
- getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
43
- if (runtimeOptions.cdpPort) {
44
- process.env.CDP_PORT = runtimeOptions.cdpPort;
45
- process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
46
- }
47
- if (runtimeOptions.watcherPort) {
48
- process.env.WATCHER_PORT = runtimeOptions.watcherPort;
49
- process.env.METRO_PORT = runtimeOptions.watcherPort;
50
- }
51
- if (runtimeOptions.launchExistingDist) {
52
- process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
37
+ function requireRecipeTrustResolver(harness) {
38
+ if (typeof harness.resolveRecipeTrustInput !== "function") {
39
+ throw usageError(
40
+ "Recipe trust enforcement requires a newer @farmslot/recipe-harness than the installed package. Next: mm-harness update"
41
+ );
53
42
  }
43
+ return harness.resolveRecipeTrustInput;
44
+ }
45
+ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
46
+ const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
47
+ adapter,
48
+ projectRoot,
49
+ runtimeOptions
50
+ );
54
51
  try {
52
+ const execution = preflightedExecution ?? await resolveRecipeExecution(
53
+ adapter,
54
+ recipe,
55
+ artifactsDir,
56
+ projectRoot,
57
+ actionManifestPath,
58
+ runtimeOptions
59
+ );
60
+ const { runner, runRequest, absoluteArtifactsDir, useFramedExtensionRecording } = execution;
61
+ await runner.preflight(runRequest);
55
62
  await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
56
- const absoluteArtifactsDir = path.resolve(artifactsDir);
57
63
  const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
58
- const recordVideo = runtimeOptions.recordVideo ?? false;
59
- const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
60
64
  const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
61
65
  record: true,
62
66
  cdpPort: runtimeOptions.cdpPort
63
67
  }) : void 0;
64
68
  try {
65
- const manifest = loadActionManifest(adapter, actionManifestPath);
66
- await validateManifest(manifest);
67
- const { createMetaMaskRunner } = await import("../runner.js");
68
- const runner = await createMetaMaskRunner(adapter, manifest, {
69
- quietStdout: runtimeOptions.stdoutIsMachineContract === true,
70
- onActionEvent: runtimeOptions.onActionEvent
71
- });
72
- const runRequest = {
73
- recipePath: path.resolve(recipe),
74
- artifactsDir: absoluteArtifactsDir,
75
- projectRoot,
76
- env: recipeRunEnv(adapter, runtimeOptions),
77
- recordVideo: useFramedExtensionRecording ? false : recordVideo,
78
- ...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
79
- };
80
69
  let result;
81
70
  try {
82
71
  result = await runner.run(runRequest);
@@ -92,12 +81,136 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
92
81
  await stopRecipeRecording(recording);
93
82
  }
94
83
  } finally {
84
+ restoreRuntimeEnvironment();
85
+ }
86
+ }
87
+ async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
88
+ const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
89
+ adapter,
90
+ projectRoot,
91
+ runtimeOptions
92
+ );
93
+ try {
94
+ const execution = await resolveRecipeExecution(
95
+ adapter,
96
+ recipe,
97
+ artifactsDir,
98
+ projectRoot,
99
+ actionManifestPath,
100
+ runtimeOptions
101
+ );
102
+ await execution.runner.preflight(execution.runRequest);
103
+ return execution;
104
+ } finally {
105
+ restoreRuntimeEnvironment();
106
+ }
107
+ }
108
+ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
109
+ const absoluteArtifactsDir = path.resolve(artifactsDir);
110
+ const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
111
+ const recordVideo = runtimeOptions.recordVideo ?? false;
112
+ const resolveRecipeTrustInput = requireRecipeTrustResolver(await importRecipeHarness());
113
+ const trust = resolveRecipeTrustInput({
114
+ sourceTrust: runtimeOptions.source?.trust,
115
+ sourceKind: runtimeOptions.source?.kind,
116
+ sourceName: runtimeOptions.source?.name,
117
+ sourceDigest: runtimeOptions.source?.digest,
118
+ approvalDigest: runtimeOptions.approval?.planDigest
119
+ });
120
+ const librarySources = executionLibrarySources(
121
+ runtimeOptions.librarySources,
122
+ trust.source?.trust ?? "trusted"
123
+ );
124
+ const { manifest, actionSources } = await resolveActionManifest(
125
+ adapter,
126
+ actionManifestPath,
127
+ librarySources,
128
+ process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR
129
+ );
130
+ const { createMetaMaskRunner } = await import("../runner.js");
131
+ const runner = await createMetaMaskRunner(adapter, manifest, {
132
+ quietStdout: runtimeOptions.stdoutIsMachineContract === true,
133
+ autoHud: trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
134
+ onActionEvent: runtimeOptions.onActionEvent,
135
+ actionSources,
136
+ // A direct engineer invocation is the explicit trust boundary for a
137
+ // task-local adapter. Inherited gateway provenance remains authoritative,
138
+ // so an untrusted worker cannot promote itself with CLI flags.
139
+ trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
140
+ });
141
+ const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
142
+ return {
143
+ runner,
144
+ absoluteArtifactsDir,
145
+ useFramedExtensionRecording,
146
+ runRequest: {
147
+ ...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
148
+ artifactsDir: absoluteArtifactsDir,
149
+ projectRoot,
150
+ inheritProcessEnv: false,
151
+ env: {
152
+ ...recipeProcessEnvironment(),
153
+ ...recipeRunEnv(adapter, runtimeOptions),
154
+ METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
155
+ },
156
+ recordVideo: useFramedExtensionRecording ? false : recordVideo,
157
+ ...trust,
158
+ ...librarySources ? { librarySources } : {}
159
+ }
160
+ };
161
+ }
162
+ function recipeProcessEnvironment() {
163
+ const env = { ...process.env };
164
+ delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
165
+ return env;
166
+ }
167
+ function executionLibrarySources(sources, invocationTrust) {
168
+ return sources?.map((source) => {
169
+ if (source.name === "metamask" || source.provenance?.trust === "trusted") return source;
170
+ return {
171
+ ...source,
172
+ provenance: {
173
+ ...source.provenance,
174
+ kind: source.provenance?.kind ?? "library",
175
+ trust: invocationTrust === "trusted" ? "trusted" : "unknown",
176
+ name: source.provenance?.name ?? source.name ?? path.basename(source.root)
177
+ }
178
+ };
179
+ });
180
+ }
181
+ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
182
+ const previousCdpPort = process.env.CDP_PORT;
183
+ const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
184
+ const previousWatcherPort = process.env.WATCHER_PORT;
185
+ const previousMetroPort = process.env.METRO_PORT;
186
+ const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
187
+ getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
188
+ if (runtimeOptions.cdpPort) {
189
+ process.env.CDP_PORT = runtimeOptions.cdpPort;
190
+ process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
191
+ }
192
+ if (runtimeOptions.watcherPort) {
193
+ process.env.WATCHER_PORT = runtimeOptions.watcherPort;
194
+ process.env.METRO_PORT = runtimeOptions.watcherPort;
195
+ }
196
+ if (runtimeOptions.launchExistingDist) {
197
+ process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
198
+ }
199
+ return () => {
95
200
  restoreEnv("CDP_PORT", previousCdpPort);
96
201
  restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
97
202
  restoreEnv("WATCHER_PORT", previousWatcherPort);
98
203
  restoreEnv("METRO_PORT", previousMetroPort);
99
204
  restoreEnv("METAMASK_RECIPE_EXTENSION_AUTOLAUNCH", previousExtensionAutolaunch);
205
+ };
206
+ }
207
+ function actionSourceMap(sources) {
208
+ const roots = {};
209
+ for (const [action, source] of sources) {
210
+ if (source.tier === "official") continue;
211
+ roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(path.dirname(path.dirname(source.manifestPath)), "actions"));
100
212
  }
213
+ return JSON.stringify(roots);
101
214
  }
102
215
  function recipeRunEnv(adapter, runtimeOptions = {}) {
103
216
  const base = {
@@ -169,12 +282,16 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
169
282
  const file = path.join(source.root, "recipes", candidate);
170
283
  if (isRecipeFile(file)) return { recipeFile: file };
171
284
  }
285
+ const listedFile = recipeFileByPublicName(path.join(source.root, "recipes"), recipeArg, adapter);
286
+ if (listedFile) return { recipeFile: listedFile };
172
287
  }
173
288
  } else {
174
289
  for (const candidate of candidates) {
175
290
  const file = recipePath(candidate);
176
291
  if (isRecipeFile(file)) return { recipeFile: file };
177
292
  }
293
+ const listedFile = recipeFileByPublicName(recipePath(""), recipeArg, adapter);
294
+ if (listedFile) return { recipeFile: listedFile };
178
295
  }
179
296
  }
180
297
  const packagedNames = libraryRecipeNames(adapter);
@@ -183,6 +300,155 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
183
300
  const suffix = packagedNames.length > 0 ? ` Library recipes for ${adapter}: ${packagedNames.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
184
301
  return { notFound: notFoundCore + suffix };
185
302
  }
303
+ function readRecipeMetadata(file) {
304
+ try {
305
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
306
+ return {
307
+ ...typeof parsed.title === "string" && parsed.title.trim() ? { title: parsed.title.trim() } : {},
308
+ ...typeof parsed.description === "string" && parsed.description.trim() ? { description: parsed.description.trim() } : {}
309
+ };
310
+ } catch {
311
+ return {};
312
+ }
313
+ }
314
+ function recipeNameFromFile(file, adapter) {
315
+ if (!file.endsWith(".recipe.json")) return void 0;
316
+ let name = file.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".");
317
+ const scope = name.split(".").pop() ?? "";
318
+ if (["mobile", "extension", "core"].includes(scope) && scope !== adapter) return void 0;
319
+ const scoped = scope === adapter;
320
+ if (scoped) name = name.slice(0, -(adapter.length + 1));
321
+ return { name, scoped };
322
+ }
323
+ function recipeFileByPublicName(recipesRoot, recipeName, adapter) {
324
+ let match;
325
+ let files;
326
+ try {
327
+ files = listRecipeFiles(recipesRoot);
328
+ } catch {
329
+ return void 0;
330
+ }
331
+ for (const file of files) {
332
+ const candidate = recipeNameFromFile(file, adapter);
333
+ if (!candidate || candidate.name !== recipeName) continue;
334
+ if (!match || candidate.scoped && !match.scoped) match = { file, scoped: candidate.scoped };
335
+ }
336
+ return match ? path.join(recipesRoot, match.file) : void 0;
337
+ }
338
+ function listRunnableRecipes(adapter, librarySources) {
339
+ const sources = librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: recipePath("") }];
340
+ const resolved = /* @__PURE__ */ new Map();
341
+ for (const source of sources) {
342
+ const recipesRoot = librarySources && librarySources.length > 0 ? path.join(source.root, "recipes") : source.root;
343
+ let files;
344
+ try {
345
+ files = listRecipeFiles(recipesRoot);
346
+ } catch {
347
+ continue;
348
+ }
349
+ const sourceName = source.name ?? path.basename(source.root);
350
+ const withinSource = /* @__PURE__ */ new Map();
351
+ for (const file of files) {
352
+ const candidate = recipeNameFromFile(file, adapter);
353
+ if (!candidate) continue;
354
+ const existing = withinSource.get(candidate.name);
355
+ if (!existing || candidate.scoped && !existing.scoped) {
356
+ withinSource.set(candidate.name, { file, scoped: candidate.scoped });
357
+ }
358
+ }
359
+ for (const [name, candidate] of withinSource) {
360
+ const winner = resolved.get(name);
361
+ if (winner) winner.shadows.push(sourceName);
362
+ else resolved.set(name, {
363
+ name,
364
+ source: sourceName,
365
+ file: path.join("recipes", candidate.file),
366
+ shadows: [],
367
+ ...readRecipeMetadata(path.join(recipesRoot, candidate.file))
368
+ });
369
+ }
370
+ }
371
+ return [...resolved.values()].sort((left, right) => left.name.localeCompare(right.name));
372
+ }
373
+ function describeRunnableRecipe(recipeArg, adapter, librarySources) {
374
+ const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
375
+ if ("notFound" in resolved) return resolved;
376
+ let document;
377
+ try {
378
+ document = JSON.parse(fs.readFileSync(resolved.recipeFile, "utf8"));
379
+ } catch (error) {
380
+ return {
381
+ unreadable: `could not read recipe ${resolved.recipeFile}: ${error instanceof Error ? error.message : String(error)}`
382
+ };
383
+ }
384
+ if (!isRecord(document)) {
385
+ return { unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.` };
386
+ }
387
+ const listed = listRunnableRecipes(adapter, librarySources).find((candidate) => candidate.name === recipeArg);
388
+ const composition = recipeComposition(document);
389
+ return {
390
+ recipe: {
391
+ name: listed?.name ?? recipeArg,
392
+ source: listed?.source ?? "file",
393
+ file: listed?.file ?? resolved.recipeFile,
394
+ shadows: listed?.shadows ?? [],
395
+ ...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
396
+ ...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {},
397
+ adapter,
398
+ path: resolved.recipeFile,
399
+ ...Object.prototype.hasOwnProperty.call(document, "schema_version") ? { schemaVersion: document.schema_version } : {},
400
+ inputs: isRecord(document.inputs) ? redactRecipeInputs(document.inputs) : {},
401
+ inputOverridesSupported: false,
402
+ actions: composition.actions,
403
+ actionsComplete: composition.actionsComplete,
404
+ flows: composition.flows
405
+ }
406
+ };
407
+ }
408
+ function recipeComposition(recipe) {
409
+ const actions = /* @__PURE__ */ new Set();
410
+ const flows = /* @__PURE__ */ new Set();
411
+ const visitNode = (value) => {
412
+ if (!isRecord(value) || typeof value.action !== "string") return;
413
+ actions.add(value.action);
414
+ if (value.action === "call" && typeof value.ref === "string") flows.add(value.ref);
415
+ };
416
+ const visitWorkflow = (value) => {
417
+ if (!isRecord(value)) return;
418
+ for (const phase of ["setup", "teardown"]) {
419
+ if (Array.isArray(value[phase])) value[phase].forEach(visitNode);
420
+ }
421
+ if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
422
+ };
423
+ visitNode(recipe.startState);
424
+ if (isRecord(recipe.validate)) visitWorkflow(recipe.validate.workflow);
425
+ const inlineFlows = isRecord(recipe.flows) ? recipe.flows : {};
426
+ const visitedFlows = /* @__PURE__ */ new Set();
427
+ let actionsComplete = true;
428
+ for (const ref of flows) {
429
+ if (visitedFlows.has(ref)) continue;
430
+ visitedFlows.add(ref);
431
+ const flow = inlineFlows[ref];
432
+ if (flow !== void 0) {
433
+ visitWorkflow(isRecord(flow) && isRecord(flow.workflow) ? flow.workflow : flow);
434
+ } else {
435
+ actionsComplete = false;
436
+ }
437
+ }
438
+ return {
439
+ actions: [...actions].sort(),
440
+ actionsComplete,
441
+ flows: [...flows].sort()
442
+ };
443
+ }
444
+ function redactRecipeInputs(inputs) {
445
+ return Object.fromEntries(
446
+ Object.entries(inputs).map(([key, value]) => [
447
+ key,
448
+ /(?:password|passphrase|secret|token|credential|mnemonic|seed|srp|vault|api[_-]?key|private[_-]?key)/iu.test(key) ? "<redacted>" : isRecord(value) ? redactRecipeInputs(value) : Array.isArray(value) ? value.map((entry) => isRecord(entry) ? redactRecipeInputs(entry) : entry) : value
449
+ ])
450
+ );
451
+ }
186
452
  function libraryRecipeNames(adapter) {
187
453
  let entries;
188
454
  try {
@@ -190,11 +456,7 @@ function libraryRecipeNames(adapter) {
190
456
  } catch {
191
457
  return [];
192
458
  }
193
- const scopes = ["mobile", "extension", "core"];
194
- const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".")).filter((n) => {
195
- const scope = n.split(".").pop() ?? "";
196
- return !scopes.includes(scope) || scope === adapter;
197
- }).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
459
+ const names = entries.map((file) => recipeNameFromFile(file, adapter)?.name).filter((name) => Boolean(name));
198
460
  return [...new Set(names)].sort();
199
461
  }
200
462
  function recipeNameCandidates(recipeArg, adapter) {
@@ -259,7 +521,11 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
259
521
  };
260
522
  }
261
523
  const findings = [];
262
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
524
+ const { manifest } = await resolveActionManifest(
525
+ adapter,
526
+ optionString(options, "actionManifest"),
527
+ librarySources
528
+ );
263
529
  let manifestOk = true;
264
530
  try {
265
531
  await validateManifest(manifest);
@@ -309,7 +575,12 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
309
575
  async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
310
576
  if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
311
577
  try {
312
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
578
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
579
+ const { manifest } = await resolveActionManifest(
580
+ adapter,
581
+ optionString(options, "actionManifest"),
582
+ librarySources
583
+ );
313
584
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
314
585
  const actions = getRecipeActionManifestActionNames(manifest);
315
586
  const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
@@ -330,16 +601,33 @@ async function resolveMetaMaskLibrarySources(libraryEntry) {
330
601
  if (typeof harness.resolveRecipeLibrarySources !== "function") {
331
602
  if (libraryEntry) {
332
603
  throw usageError(
333
- "--library requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support / resolveRecipeLibrarySources). The pinned 0.3.0 lacks it, and npm 0.3.2 still does not export it \u2014 recipe-library support is PENDING PUBLISH from farmslot."
604
+ "--library requires a current @farmslot/recipe-harness installation that exports recipe-library support. Next: mm-harness update"
334
605
  );
335
606
  }
336
607
  return void 0;
337
608
  }
609
+ const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
338
610
  const sources = await harness.resolveRecipeLibrarySources(
339
- libraryEntry ? { cliEntries: [libraryEntry] } : void 0
611
+ cliEntries.length > 0 ? { cliEntries } : void 0
340
612
  );
341
- sources.push({ name: "metamask", root: path.join(runnerDir, "library") });
342
- return sources;
613
+ const canonicalRoot = path.resolve(runnerDir, "library");
614
+ const configuredCanonical = sources.find((source) => source.name === "metamask");
615
+ if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
616
+ throw usageError(
617
+ `Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
618
+ );
619
+ }
620
+ if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
621
+ sources.push({ name: "metamask", root: canonicalRoot });
622
+ }
623
+ return sources.map((source) => ({
624
+ ...source,
625
+ provenance: source.provenance ?? {
626
+ kind: source.name === "metamask" ? "bundled" : "library",
627
+ trust: source.name === "metamask" ? "trusted" : "unknown",
628
+ name: source.name ?? path.basename(source.root)
629
+ }
630
+ }));
343
631
  }
344
632
  function synthesizeOneNodeRecipe(action, args) {
345
633
  return {
@@ -350,7 +638,12 @@ function synthesizeOneNodeRecipe(action, args) {
350
638
  workflow: {
351
639
  entry: "call",
352
640
  nodes: {
353
- call: { action, ...args, next: "done", intent: `Call ${action} in isolation` },
641
+ call: {
642
+ ...args,
643
+ action,
644
+ next: "done",
645
+ intent: typeof args.intent === "string" ? args.intent : `Call ${action} in isolation`
646
+ },
354
647
  done: { action: "end", status: "pass" }
355
648
  }
356
649
  }
@@ -358,7 +651,8 @@ function synthesizeOneNodeRecipe(action, args) {
358
651
  };
359
652
  }
360
653
  async function runOneNode(adapter, action, args, target, actionManifest) {
361
- const manifest = loadActionManifest(adapter, actionManifest);
654
+ const librarySources = await resolveMetaMaskLibrarySources(void 0);
655
+ const { manifest } = await resolveActionManifest(adapter, actionManifest, librarySources);
362
656
  const recipe = synthesizeOneNodeRecipe(action, args);
363
657
  const validation = await validateRecipeAdapterAware(recipe, manifest);
364
658
  if (validation.status === "invalid") return { status: "fail" };
@@ -367,7 +661,7 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
367
661
  fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
368
662
  `);
369
663
  const result = await runRecipe(adapter, recipeFile, path.join(scratch, "artifacts"), target, actionManifest, {
370
- librarySources: await resolveMetaMaskLibrarySources(void 0),
664
+ librarySources,
371
665
  stdoutIsMachineContract: true
372
666
  });
373
667
  return { status: result.status === "pass" ? "pass" : "fail" };
@@ -495,21 +789,27 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
495
789
  return { status: result2.status === "pass" ? 0 : EXIT.runtime, output: result2.status };
496
790
  });
497
791
  const result = await restart();
498
- const recorded = result.status === 0 && record(initial.fingerprint);
792
+ const platform = resolveRunMobilePlatform();
793
+ const bridge = result.status === 0 ? await (deps.waitForBridge ? deps.waitForBridge() : spawnScriptStreaming(
794
+ path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh"),
795
+ ["--target", target, "--platform", platform],
796
+ target
797
+ )) : { status: result.status, output: result.output };
798
+ const recorded = result.status === 0 && bridge.status === 0 && record(initial.fingerprint);
499
799
  const verified = recorded ? check() : initial;
500
- if (result.status !== 0 || !recorded || verified.status !== "current") {
501
- const message = "Mobile source restart did not produce a current loaded-source baseline.";
800
+ if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
801
+ const message = bridge.status !== 0 ? "Mobile source restart completed but the platform-matched bridge did not become ready." : "Mobile source restart did not produce a current loaded-source baseline.";
502
802
  if (json) {
503
803
  console.log(JSON.stringify({
504
804
  schemaVersion: 1,
505
805
  status: "fail",
506
806
  recoverable: false,
507
807
  attemptedRecoveries: state.attemptedRecoveries,
508
- error: { code: "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: result.output }
808
+ error: { code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: bridge.output || result.output }
509
809
  }, null, 2));
510
810
  } else {
511
811
  console.error(`\u2717 ${message}`);
512
- console.error(` Next: ${userAction}`);
812
+ console.error(` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`);
513
813
  }
514
814
  return EXIT.infra;
515
815
  }
@@ -517,6 +817,12 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
517
817
  state.mutations.push({ type: "runtime", action: "restarted", reasonCode: `source-${initial.status}` });
518
818
  return null;
519
819
  }
820
+ function resolveRunMobilePlatform() {
821
+ if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
822
+ return process.env.PLATFORM;
823
+ }
824
+ return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
825
+ }
520
826
  async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
521
827
  const decide = deps.decide ?? (async () => {
522
828
  const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
@@ -568,6 +874,24 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
568
874
  return EXIT.infra;
569
875
  }
570
876
  const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
877
+ const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
878
+ if (productConfigBlock) {
879
+ const message = productConfigBlock.message;
880
+ const userAction2 = productConfigBlock.userAction;
881
+ if (json) {
882
+ console.log(JSON.stringify({
883
+ schemaVersion: 1,
884
+ status: "fail",
885
+ recoverable: false,
886
+ mutations: state.mutations,
887
+ error: { code: "EXTENSION_PRODUCT_CONFIG_REQUIRED", message, userAction: userAction2 }
888
+ }, null, 2));
889
+ } else {
890
+ console.error(`\u2717 ${message}`);
891
+ console.error(` Next: ${userAction2}`);
892
+ }
893
+ return EXIT.infra;
894
+ }
571
895
  state.attemptedRecoveries.push(recoveryCode);
572
896
  deps.onRecovery?.(recoveryCode);
573
897
  if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
@@ -690,12 +1014,16 @@ function countRecipeNodes(recipe) {
690
1014
  }
691
1015
  export {
692
1016
  countRecipeNodes,
1017
+ describeRunnableRecipe,
693
1018
  emitHealViolation,
694
1019
  ensureExtensionProofRuntime,
695
1020
  ensureMobileProofRuntime,
696
1021
  executeWithHealBounds,
1022
+ listRunnableRecipes,
1023
+ preflightRecipe,
697
1024
  prepareHeal,
698
1025
  recoverRunInfra,
1026
+ requireRecipeTrustResolver,
699
1027
  resolveMetaMaskLibrarySources,
700
1028
  resolveRunRecipeArg,
701
1029
  runOneNode,