@deeeed/metamask-harness 0.17.5 → 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 (132) hide show
  1. package/CHANGELOG.md +872 -0
  2. package/README.md +94 -140
  3. package/adapters/extension/console-tail.mjs +55 -24
  4. package/adapters/extension/ensure-browser.sh +6 -1
  5. package/adapters/extension/inject.mjs +6 -9
  6. package/adapters/extension/launch-browser.cjs +7 -1
  7. package/adapters/extension/launch-webpack.cjs +83 -0
  8. package/adapters/extension/launch.sh +8 -11
  9. package/adapters/extension/lib/chrome-args.cjs +8 -1
  10. package/adapters/extension/lib/macos-focus.cjs +32 -0
  11. package/adapters/extension/live.sh +12 -21
  12. package/adapters/extension/reattach.sh +1 -0
  13. package/adapters/extension/seed-fixture.sh +4 -12
  14. package/adapters/extension/sidepanel-toggle.sh +4 -1
  15. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  16. package/adapters/extension/start-watch.sh +17 -4
  17. package/adapters/extension/stop-viewers.sh +1 -1
  18. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  19. package/adapters/extension/verify.sh +1 -1
  20. package/adapters/manifest.json +53 -37
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  22. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  25. package/adapters/mobile/cleanup.sh +16 -176
  26. package/adapters/mobile/inject.sh +90 -671
  27. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  28. package/adapters/mobile/open-device.sh +45 -7
  29. package/adapters/mobile/start-console-forwarder.sh +70 -0
  30. package/adapters/mobile/start-metro.sh +0 -41
  31. package/adapters/mobile/verify.sh +19 -64
  32. package/adapters/mobile/wait-for-bridge.sh +22 -8
  33. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  34. package/adapters/shared/resolve-slot-ports.sh +17 -5
  35. package/adapters/shared/update-check-worker.mjs +43 -0
  36. package/bin/mm-harness +21 -2
  37. package/dist/adapters/core/surface.js +4 -1
  38. package/dist/adapters/extension/console-capture.js +95 -0
  39. package/dist/adapters/extension/product-config.js +110 -0
  40. package/dist/adapters/extension/runtime-decision.js +20 -2
  41. package/dist/adapters/extension/surface.js +22 -3
  42. package/dist/adapters/mobile/perps-env.js +43 -3
  43. package/dist/adapters/mobile/prepare.js +39 -7
  44. package/dist/adapters/mobile/surface.js +5 -2
  45. package/dist/adapters/resolve-slot-ports.js +2 -2
  46. package/dist/adapters/slot-ports.js +13 -16
  47. package/dist/adapters.js +56 -16
  48. package/dist/checkout-lock.js +27 -2
  49. package/dist/cli-color.js +19 -0
  50. package/dist/cli-commands.js +1 -1
  51. package/dist/cli.js +7 -14
  52. package/dist/command-contract.js +451 -0
  53. package/dist/command-journal.js +225 -0
  54. package/dist/commands/call.js +170 -50
  55. package/dist/commands/check.js +9 -3
  56. package/dist/commands/completion-candidates.js +20 -13
  57. package/dist/commands/device-target.js +27 -12
  58. package/dist/commands/doctor.js +106 -25
  59. package/dist/commands/fixtures.js +92 -34
  60. package/dist/commands/flows.js +39 -10
  61. package/dist/commands/last.js +52 -0
  62. package/dist/commands/launch/extension.js +38 -15
  63. package/dist/commands/launch/index.js +207 -63
  64. package/dist/commands/list-executables.js +151 -29
  65. package/dist/commands/logs.js +8 -6
  66. package/dist/commands/manifest.js +270 -35
  67. package/dist/commands/parse-args.js +13 -1
  68. package/dist/commands/provision.js +10 -3
  69. package/dist/commands/run-engine.js +435 -105
  70. package/dist/commands/run-report.js +12 -3
  71. package/dist/commands/run.js +355 -50
  72. package/dist/commands/shared.js +75 -3
  73. package/dist/commands/status-probe.js +4 -1
  74. package/dist/commands/status.js +2 -1
  75. package/dist/commands/stop.js +7 -2
  76. package/dist/commands/update.js +16 -28
  77. package/dist/completions-cache.js +1 -1
  78. package/dist/doctor.js +57 -10
  79. package/dist/harness.js +47 -137
  80. package/dist/heal-bounds.js +2 -2
  81. package/dist/json-stream.js +57 -0
  82. package/dist/live-adapter-contract.js +138 -32
  83. package/dist/manifest.js +161 -1
  84. package/dist/mm-harness-cli.js +135 -54
  85. package/dist/paths.js +2 -5
  86. package/dist/recipe-security.js +178 -0
  87. package/dist/run-diagnostics.js +261 -0
  88. package/dist/runner.js +117 -8
  89. package/docs/CONTRIBUTING.md +137 -0
  90. package/docs/QA.md +185 -0
  91. package/docs/RECIPES.md +161 -0
  92. package/docs/SECURITY.md +88 -0
  93. package/library/README.md +4 -0
  94. package/library/actions/core/perps/_controller.mjs +10 -55
  95. package/library/actions/core/perps/read_account.mjs +2 -2
  96. package/library/actions/core/perps/read_orders.mjs +2 -1
  97. package/library/actions/core/perps/read_positions.mjs +2 -1
  98. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  99. package/library/actions/extension/platform/cdp.mjs +1 -0
  100. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  101. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  102. package/library/manifests/core.action-manifest.json +68 -7
  103. package/library/manifests/extension.action-manifest.json +53 -0
  104. package/library/manifests/mobile.action-manifest.json +81 -3
  105. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  106. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  107. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  108. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  109. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  110. package/package.json +12 -5
  111. package/scripts/completions.sh +7 -7
  112. package/scripts/validate-human-outcomes.mjs +169 -0
  113. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  114. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  115. package/adapters/mobile/overlay/compat/README.md +0 -28
  116. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  117. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  118. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  119. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  120. package/docs/ADAPTER-SURFACE.md +0 -119
  121. package/docs/CHEATSHEET.md +0 -61
  122. package/docs/CLI-SPEC.md +0 -1098
  123. package/docs/CODE-MAP.md +0 -62
  124. package/docs/DEBUG-HANDOVER.md +0 -36
  125. package/docs/MENTAL-MODEL.md +0 -295
  126. package/docs/UX-PRINCIPLES.md +0 -64
  127. package/docs/architecture.md +0 -398
  128. package/docs/live-adapter-contract.md +0 -188
  129. package/docs/package-boundaries.md +0 -47
  130. package/docs/perps-flow-catalog.md +0 -235
  131. package/docs/recipe-libraries.md +0 -301
  132. package/docs/runtime-file-conventions.md +0 -36
@@ -1,12 +1,11 @@
1
1
  import fs from "node:fs";
2
- import { mkdtemp } from "node:fs/promises";
3
- import os from "node:os";
2
+ import { createHash } from "node:crypto";
4
3
  import path from "node:path";
5
- import { loadActionManifest } from "../manifest.js";
4
+ import { resolveActionManifest } from "../manifest.js";
6
5
  import { importRecipeProtocol } from "../paths.js";
7
6
  import { recipeRunning } from "../heal-bounds.js";
8
7
  import { color } from "../cli-color.js";
9
- import { EXIT } from "./shared.js";
8
+ import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
10
9
  import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
11
10
  import {
12
11
  parseArgs,
@@ -15,6 +14,7 @@ import {
15
14
  optionString,
16
15
  resolveAdapter,
17
16
  runtimeOptionsFromCli,
17
+ shellQuote,
18
18
  usageError
19
19
  } from "./parse-args.js";
20
20
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -27,11 +27,14 @@ import {
27
27
  prepareHeal,
28
28
  recoverRunInfra,
29
29
  resolveMetaMaskLibrarySources,
30
+ preflightRecipe,
30
31
  runRecipe,
31
32
  synthesizeOneNodeRecipe,
32
33
  validateRecipeAdapterAware
33
34
  } from "./run-engine.js";
34
35
  import { acquireCheckoutLock } from "../checkout-lock.js";
36
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
37
+ import { recipeTrustFailure } from "../recipe-security.js";
35
38
  async function handleCall(argv) {
36
39
  if (argv.includes("--list")) {
37
40
  const { options: options2 } = parseArgs(argv, "call");
@@ -50,7 +53,8 @@ async function handleCall(argv) {
50
53
  let discovery = "mm-harness actions";
51
54
  try {
52
55
  const { adapter: adapter2 } = resolveAdapter(options);
53
- const manifest2 = loadActionManifest(adapter2, optionString(options, "actionManifest"));
56
+ const librarySources2 = await resolveMetaMaskLibrarySources(optionString(options, "library"));
57
+ const { manifest: manifest2 } = await resolveActionManifest(adapter2, optionString(options, "actionManifest"), librarySources2);
54
58
  const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
55
59
  const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
56
60
  example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
@@ -59,44 +63,65 @@ async function handleCall(argv) {
59
63
  }
60
64
  const message = `call requires <action>. Example: ${example}`;
61
65
  const userAction = `${example} # see the vocabulary: ${discovery}`;
62
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", error: { code: "USAGE", message, userAction } }, null, 2));
66
+ if (json) console.log(JSON.stringify({
67
+ schemaVersion: 1,
68
+ command: "call",
69
+ status: "fail",
70
+ error: { code: "CLI_MISSING_POSITIONAL", message, userAction },
71
+ exitCode: EXIT.usage
72
+ }, null, 2));
63
73
  else console.error(`${message}
64
74
  See the vocabulary: ${discovery}`);
65
75
  return EXIT.usage;
66
76
  }
67
77
  const { adapter, target } = resolveAdapter(options);
78
+ if (!fs.existsSync(target)) {
79
+ return usageOut(
80
+ json,
81
+ "call",
82
+ `target does not exist: ${target}`,
83
+ "pass --target <metamask-checkout> pointing to an existing checkout"
84
+ );
85
+ }
68
86
  getAdapterSurface(adapter).resolveSlotPorts(target);
69
87
  const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
70
88
  if ("code" in dtResult) {
71
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
72
- else console.error(`\u2717 call: ${dtResult.message}`);
89
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
90
+ else console.error(`\u2717 call: ${dtResult.message}
91
+ Next: ${dtResult.userAction}`);
73
92
  return EXIT.usage;
74
93
  }
75
94
  if (recipeRunning(target)) {
76
95
  const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
96
+ const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
77
97
  if (json) {
78
- console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
98
+ console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
79
99
  } else {
80
- console.error(`\u2717 mm-harness call: ${msg}`);
100
+ console.error(`\u2717 mm-harness call: ${msg}
101
+ Next: ${userAction}`);
81
102
  }
82
103
  return EXIT.bounded;
83
104
  }
84
105
  const actionManifestOverride = optionString(options, "actionManifest");
85
- const manifest = loadActionManifest(adapter, actionManifestOverride);
106
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
107
+ const { manifest } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
86
108
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
87
109
  const names = getRecipeActionManifestActionNames(manifest);
88
110
  const resolution = resolveActionName(shortName, names);
89
111
  if (resolution.status === "unknown") {
90
- const message = `\u2717 call: unknown action "${shortName}" for the ${adapter} adapter.
91
- See the vocabulary: mm-harness actions --adapter ${adapter} --json`;
92
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message } }, null, 2));
93
- else console.error(message);
112
+ const message = `unknown action "${shortName}" for the ${adapter} adapter.`;
113
+ const userAction = `mm-harness actions --adapter ${adapter} --json`;
114
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
115
+ else console.error(`\u2717 call: ${message}
116
+ Next: ${userAction}`);
94
117
  return EXIT.usage;
95
118
  }
96
119
  if (resolution.status === "ambiguous") {
97
- const message = `\u2717 call: "${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
98
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates } }, null, 2));
99
- else console.error(message);
120
+ const message = `"${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
121
+ const userAction = `mm-harness actions --action ${resolution.candidates[0]} --adapter ${adapter} --json`;
122
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates, userAction } }, null, 2));
123
+ else console.error(`\u2717 call: ${message}
124
+ Next: ${userAction}`);
100
125
  return EXIT.usage;
101
126
  }
102
127
  const resolvedAction = resolution.resolved;
@@ -113,19 +138,74 @@ async function handleCall(argv) {
113
138
  const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
114
139
  const validation = await validateRecipeAdapterAware(recipe, manifest);
115
140
  if (validation.status === "invalid") {
116
- const message = `\u2717 call ${resolvedAction}: recipe validation failed`;
117
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message } }, null, 2));
141
+ const message = `call ${resolvedAction}: recipe validation failed`;
142
+ const userAction = `mm-harness actions --action ${resolvedAction} --adapter ${adapter} --json`;
143
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
118
144
  else {
119
- console.error(message);
145
+ console.error(`\u2717 ${message}`);
120
146
  for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
147
+ console.error(` Next: ${userAction}`);
148
+ }
149
+ return EXIT.validation;
150
+ }
151
+ const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction, args);
152
+ const requestedRuntimeOptions = runtimeOptionsFromCli(options);
153
+ const callRuntimeOptions = {
154
+ ...requestedRuntimeOptions,
155
+ ...librarySources ? { librarySources } : {},
156
+ autoHud: false,
157
+ stdoutIsMachineContract: json,
158
+ source: requestedRuntimeOptions.source ?? {
159
+ kind: "operator",
160
+ trust: "trusted",
161
+ name: "mm-harness call"
162
+ }
163
+ };
164
+ let preflightedExecution;
165
+ try {
166
+ preflightedExecution = await preflightRecipe(
167
+ adapter,
168
+ recipe,
169
+ artifactsDir,
170
+ target,
171
+ actionManifestOverride,
172
+ callRuntimeOptions
173
+ );
174
+ } catch (error) {
175
+ const trustFailure = recipeTrustFailure(error);
176
+ if (!trustFailure) throw error;
177
+ const planDigest = trustFailure.details?.recipeDigest;
178
+ if (planDigest) {
179
+ trustFailure.userAction = `review the plan, then rerun this call with --artifacts-dir ${shellQuote(artifactsDir)} --approve-plan ${shellQuote(planDigest)}; managed callers must keep the same artifact directory and execution environment, then set FARMSLOT_RECIPE_APPROVE_PLAN=${shellQuote(planDigest)}`;
180
+ }
181
+ if (json) {
182
+ console.log(JSON.stringify({
183
+ schemaVersion: 1,
184
+ command: "call",
185
+ status: "fail",
186
+ error: trustFailure,
187
+ exitCode: EXIT.validation
188
+ }, null, 2));
189
+ } else {
190
+ console.error(`\u2717 mm-harness call: ${trustFailure.message}`);
191
+ const blocked = trustFailure.details?.blocked;
192
+ if (blocked?.length) {
193
+ console.error(" Restricted plan nodes:");
194
+ for (const node of blocked.slice(0, 10)) {
195
+ const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
196
+ console.error(
197
+ ` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
198
+ );
199
+ }
200
+ if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
201
+ }
202
+ console.error(` Next: ${trustFailure.userAction}`);
121
203
  }
122
204
  return EXIT.validation;
123
205
  }
124
206
  const lock = acquireCheckoutLock(target, "call");
125
207
  if ("message" in lock) {
126
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "SANDBOX_BUSY", message: lock.message } }, null, 2));
127
- else console.error(`\u2717 mm-harness call: ${lock.message}`);
128
- return EXIT.bounded;
208
+ return checkoutBusyOut(json, "call", lock.message, lock.path);
129
209
  }
130
210
  try {
131
211
  const prepared = await prepareHeal(adapter, target, options, json, {
@@ -133,19 +213,20 @@ async function handleCall(argv) {
133
213
  });
134
214
  if (typeof prepared === "number") return prepared;
135
215
  const { state, heal } = prepared;
136
- const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
137
- const recipeFile = path.join(scratch, "call.recipe.json");
138
- fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
139
- `);
140
- const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
141
- const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
142
- const callRuntimeOptions = {
143
- ...runtimeOptionsFromCli(options),
144
- ...librarySources ? { librarySources } : {},
145
- stdoutIsMachineContract: json
146
- };
147
216
  const { result, violation } = await executeWithHealBounds(
148
- () => runRecipe(adapter, recipeFile, artifactsDir, target, actionManifestOverride, callRuntimeOptions),
217
+ () => {
218
+ const execution = preflightedExecution;
219
+ preflightedExecution = void 0;
220
+ return runRecipe(
221
+ adapter,
222
+ recipe,
223
+ artifactsDir,
224
+ target,
225
+ actionManifestOverride,
226
+ callRuntimeOptions,
227
+ execution
228
+ );
229
+ },
149
230
  adapter,
150
231
  target,
151
232
  heal,
@@ -154,6 +235,9 @@ async function handleCall(argv) {
154
235
  );
155
236
  if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
156
237
  const callOutput = readCallOutput(result.tracePath);
238
+ const safeArgs = redactCallValue(args);
239
+ const safeCallOutput = redactCallValue(callOutput);
240
+ const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
157
241
  if (json) {
158
242
  console.log(
159
243
  JSON.stringify(
@@ -163,30 +247,41 @@ async function handleCall(argv) {
163
247
  adapter,
164
248
  action: shortName,
165
249
  resolvedAction,
166
- args,
250
+ args: safeArgs,
167
251
  status: result.status,
168
252
  summaryPath: result.summaryPath,
169
253
  tracePath: result.tracePath,
170
254
  artifactManifestPath: result.artifactManifestPath,
171
- ...callOutput !== void 0 ? { output: callOutput } : {},
255
+ ...result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {},
256
+ ...result.sideFindings ? { sideFindings: result.sideFindings } : {},
257
+ ...callOutput !== void 0 ? { output: safeCallOutput } : {},
172
258
  recovered: state.recovered,
173
259
  mutations: state.mutations,
174
- exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
260
+ exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime,
261
+ ...result.status === "fail" ? { error: { code: "ACTION_EXECUTION_FAILED", message: `${resolvedAction} failed; inspect the persisted result and evidence paths`, userAction: failureUserAction } } : {}
175
262
  },
176
263
  null,
177
264
  2
178
265
  )
179
266
  );
180
267
  } else {
181
- const rendered = callOutput !== void 0 ? `
182
- Result:
183
- ${formatCallOutput(callOutput)}` : "";
268
+ const renderedInputs = Object.keys(args).length > 0 ? formatCallOutput(safeArgs) : "";
184
269
  const out = (style, text) => color(style, text, { stream: process.stdout });
270
+ const diagnostics = formatRunDiagnosticsForHuman(
271
+ readRunDiagnosticsDocument(result.diagnosticsPath),
272
+ adapter
273
+ );
274
+ const renderedDiagnostics = `${out("label", "Diagnostics:")}
275
+ ${diagnostics.map((line) => ` ${line}`).join("\n")}
276
+ `;
185
277
  console.log(
186
- `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
278
+ `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${renderedInputs ? `
279
+ ${out("label", "Inputs:")}
280
+ ${renderedInputs}` : ""}${callOutput !== void 0 ? `
187
281
  ${out("label", "Result:")}
188
- ${formatCallOutput(callOutput)}` : ""}
189
- ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
282
+ ${formatCallOutput(safeCallOutput)}` : ""}
283
+ ` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
284
+ Next: ${failureUserAction}` : "")
190
285
  );
191
286
  }
192
287
  return result.status === "pass" ? EXIT.ok : EXIT.runtime;
@@ -194,6 +289,11 @@ ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
194
289
  lock.release();
195
290
  }
196
291
  }
292
+ function defaultCallArtifactsDir(target, action, args) {
293
+ const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
294
+ const digest = createHash("sha256").update(JSON.stringify({ action, args })).digest("hex").slice(0, 12);
295
+ return path.join(target, "temp", "recipe", "calls", `${actionStem}-${digest}`);
296
+ }
197
297
  function readCallOutput(tracePath) {
198
298
  try {
199
299
  const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
@@ -209,20 +309,32 @@ function formatCallOutput(value) {
209
309
  if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
210
310
  return JSON.stringify(value, null, 2);
211
311
  }
312
+ function redactCallValue(value, key = "") {
313
+ if (/(?:password|secret|mnemonic|seed|srp|vault|private[_-]?key)/iu.test(key)) return "<redacted>";
314
+ if (Array.isArray(value)) return value.map((entry) => redactCallValue(entry));
315
+ if (isRecord(value)) {
316
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactCallValue(entry, entryKey)]));
317
+ }
318
+ return value;
319
+ }
212
320
  async function handleCallHelp(argv, genericHelp) {
213
321
  const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
214
322
  const { options } = parseArgs(rest, "call");
215
323
  let adapter;
216
324
  let manifest;
325
+ let actionSources = /* @__PURE__ */ new Map();
217
326
  try {
218
327
  ({ adapter } = resolveAdapter(options));
219
- manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
328
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
329
+ const resolution = await resolveActionManifest(adapter, optionString(options, "actionManifest"), librarySources);
330
+ manifest = resolution.manifest;
331
+ actionSources = resolution.actionSources;
220
332
  } catch {
221
333
  process.stdout.write(`${genericHelp}
222
334
  `);
223
335
  return EXIT.ok;
224
336
  }
225
- const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest), shortName) : [];
337
+ const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest, actionSources), shortName) : [];
226
338
  if (matches.length === 0) {
227
339
  process.stdout.write(`${genericHelp}
228
340
  `);
@@ -250,7 +362,8 @@ function renderCallActionHelp(entry) {
250
362
  );
251
363
  const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
252
364
  if (entry.description) lines.push(` ${entry.description}`, "");
253
- const names = Object.keys(properties).sort();
365
+ lines.push(` Source: ${entry.source}${entry.sourceManifest ? ` (${entry.sourceManifest})` : ""}`, "");
366
+ const names = Object.keys(properties).filter((name) => name !== "action" && name !== "next").sort();
254
367
  if (names.length === 0) {
255
368
  lines.push(" Fields: (none)");
256
369
  } else {
@@ -320,7 +433,7 @@ function parseCallValue(value) {
320
433
  const number = Number(value);
321
434
  if (Number.isFinite(number)) return number;
322
435
  }
323
- if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
436
+ if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}") || value.startsWith('"') && value.endsWith('"')) {
324
437
  try {
325
438
  return JSON.parse(value);
326
439
  } catch {
@@ -340,6 +453,7 @@ function isForwardedOptionValue(argv, index) {
340
453
  }
341
454
  const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
342
455
  "action-manifest",
456
+ "approve-plan",
343
457
  "adapter",
344
458
  "artifacts-dir",
345
459
  "cdp-port",
@@ -350,6 +464,10 @@ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
350
464
  "platform",
351
465
  "project-root",
352
466
  "slot",
467
+ "source-digest",
468
+ "source-kind",
469
+ "source-name",
470
+ "source-trust",
353
471
  "target",
354
472
  "validation-runtime-dir",
355
473
  "watcher-port"
@@ -373,6 +491,8 @@ function pickCallExampleAction(names) {
373
491
  return names[0] ?? "command";
374
492
  }
375
493
  export {
494
+ defaultCallArtifactsDir,
376
495
  handleCall,
377
- handleCallHelp
496
+ handleCallHelp,
497
+ redactCallValue
378
498
  };
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { detectAdapter } from "../harness.js";
5
5
  import { EXIT, usageOut } from "./shared.js";
6
- import { optionFlag, optionString, parseArgs } from "./parse-args.js";
6
+ import { optionFlag, optionString, parseArgs, shellQuote } from "./parse-args.js";
7
7
  const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
8
8
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
9
9
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
@@ -111,6 +111,7 @@ async function handleCheck(argv) {
111
111
  const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
112
112
  const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
113
113
  const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === "eslint" && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
114
+ const failureUserAction = guidance[0] ?? `inspect ${path.join(artifactsDir, "validation-summary.json")}, fix the failing check, then retry: mm-harness check diff --target ${shellQuote(target)} --base ${shellQuote(baseRef)} --profile ${profile} --json`;
114
115
  const summary = {
115
116
  schemaVersion: 1,
116
117
  command: "check",
@@ -129,6 +130,7 @@ async function handleCheck(argv) {
129
130
  fixes,
130
131
  checks,
131
132
  guidance,
133
+ ...status === "fail" ? { error: { code: "CHECK_DIFF_FAILED", message: "one or more bounded checks failed", userAction: failureUserAction } } : {},
132
134
  artifacts: {
133
135
  dir: artifactsDir,
134
136
  summaryJson: path.join(artifactsDir, "validation-summary.json"),
@@ -518,10 +520,14 @@ function renderHuman(summary) {
518
520
  if (check.status === "fail" && check.logPath) console.log(` log: ${check.logPath}`);
519
521
  }
520
522
  for (const item of summary.guidance) console.log(` Next: ${item}`);
523
+ if (summary.guidance.length === 0 && summary.status === "fail" && summary.error?.userAction) {
524
+ console.log(` Next: ${summary.error.userAction}`);
525
+ }
521
526
  console.log(`Artifacts: ${summary.artifacts.summaryJson}`);
522
527
  }
523
528
  function failEnvelope(input) {
524
529
  const exitCode = EXIT.usage;
530
+ const userAction = USAGE;
525
531
  const body = {
526
532
  schemaVersion: 1,
527
533
  command: "check",
@@ -534,11 +540,11 @@ function failEnvelope(input) {
534
540
  baseRef: input.baseRef,
535
541
  baseSource: "unknown",
536
542
  artifacts: { dir: input.artifactsDir },
537
- error: { code: input.code, message: input.message }
543
+ error: { code: input.code, message: input.message, userAction }
538
544
  };
539
545
  if (input.json) console.log(JSON.stringify(body, null, 2));
540
546
  else console.error(`\u2717 check diff: ${input.message}
541
- Next: ${USAGE}`);
547
+ Next: ${userAction}`);
542
548
  return exitCode;
543
549
  }
544
550
  export {
@@ -1,4 +1,4 @@
1
- import { loadActionManifest } from "../manifest.js";
1
+ import { resolveActionManifest } from "../manifest.js";
2
2
  import {
3
3
  invalidateCompletionCache,
4
4
  readFreshCandidates,
@@ -13,13 +13,13 @@ import {
13
13
  resolveAdapter,
14
14
  targetPath
15
15
  } from "./parse-args.js";
16
- import { resolveMetaMaskLibrarySources } from "./run-engine.js";
16
+ import { listRunnableRecipes, resolveMetaMaskLibrarySources } from "./run-engine.js";
17
17
  async function handleCompletionCandidates(argv) {
18
18
  const kind = argv[0];
19
19
  const { options } = parseArgs(argv.slice(1), "completion-candidates");
20
20
  const json = optionFlag(options, "json");
21
- if (kind !== "actions" && kind !== "flows") {
22
- console.error("completion-candidates requires <actions|flows>.");
21
+ if (kind !== "actions" && kind !== "flows" && kind !== "recipes") {
22
+ console.error("completion-candidates requires <actions|flows|recipes>.");
23
23
  return EXIT.usage;
24
24
  }
25
25
  const target = targetPath(options);
@@ -27,17 +27,15 @@ async function handleCompletionCandidates(argv) {
27
27
  let candidates;
28
28
  if (kind === "actions") {
29
29
  adapter = resolveAdapter(options).adapter;
30
- candidates = readFreshCandidates(target, "actions");
31
- if (!candidates) {
32
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
33
- const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
34
- candidates = getRecipeActionManifestActionNames(manifest);
35
- writeCompletionCandidates(target, "actions", candidates);
36
- }
37
- } else {
30
+ const sources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
31
+ const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
32
+ const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
33
+ candidates = getRecipeActionManifestActionNames(manifest);
34
+ writeCompletionCandidates(target, "actions", candidates);
35
+ } else if (kind === "flows") {
38
36
  candidates = readFreshCandidates(target, "flows");
39
37
  if (!candidates) {
40
- const sources = await resolveMetaMaskLibrarySources(void 0).catch(() => void 0);
38
+ const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
41
39
  if (!sources) {
42
40
  candidates = [];
43
41
  } else {
@@ -47,6 +45,15 @@ async function handleCompletionCandidates(argv) {
47
45
  writeCompletionCandidates(target, "flows", candidates);
48
46
  }
49
47
  }
48
+ } else {
49
+ adapter = resolveAdapter(options).adapter;
50
+ const cacheKey = `recipes:${adapter}`;
51
+ candidates = readFreshCandidates(target, cacheKey);
52
+ if (!candidates) {
53
+ const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
54
+ candidates = sources ? listRunnableRecipes(adapter, sources).map((recipe) => recipe.name) : [];
55
+ writeCompletionCandidates(target, cacheKey, candidates);
56
+ }
50
57
  }
51
58
  if (json) console.log(JSON.stringify({ schemaVersion: 1, kind, adapter, candidates }, null, 2));
52
59
  else for (const candidate of candidates) console.log(candidate);
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { listConnectedDevices } from "../devices.js";
3
- import { optionString } from "./parse-args.js";
3
+ import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
4
4
  function normalizeDeviceName(value) {
5
5
  return value.replace(/_/gu, " ").trim();
6
6
  }
@@ -75,6 +75,12 @@ function configuredPinForPlatform(platform) {
75
75
  function oppositePlatform(platform) {
76
76
  return platform === "ios" ? "android" : "ios";
77
77
  }
78
+ function deviceProbe(options) {
79
+ return `mm-harness status --target ${shellQuoteArg(targetPath(options))} --all-devices --json`;
80
+ }
81
+ function deviceRecovery(options, rerun) {
82
+ return rerun ? `${deviceProbe(options)}; then retry: ${rerun}` : deviceProbe(options);
83
+ }
78
84
  function applyDeviceTargeting(command, adapter, options, opts) {
79
85
  const device = optionString(options, "device");
80
86
  const platformPreference = mobilePlatformPreference(options);
@@ -84,7 +90,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
84
90
  ok: false,
85
91
  code: "DEVICE_WRONG_ADAPTER",
86
92
  message: `--device is only supported on the mobile adapter (the ${adapter} adapter has no device to target).
87
- Drop --device, or run this ${command} inside a metamask-mobile checkout.`
93
+ Drop --device, or run this ${command} inside a metamask-mobile checkout.`,
94
+ userAction: `remove --device; ${adapter} has no device target`
88
95
  };
89
96
  }
90
97
  return { ok: true };
@@ -97,7 +104,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
97
104
  ok: false,
98
105
  code: "DEVICE_PLATFORM_CONFLICT",
99
106
  message: `--device ${device} resolves to ${matched.platform}, but --platform ${platformPreference} was requested.
100
- To use this device intentionally, drop --platform ${platformPreference} or choose a ${platformPreference} device.`
107
+ To use this device intentionally, drop --platform ${platformPreference} or choose a ${platformPreference} device.`,
108
+ userAction: deviceRecovery(options, opts.rerun)
101
109
  };
102
110
  }
103
111
  if (matched.platform === "android") setAndroidDeviceEnv(matched.id, matched.name);
@@ -126,7 +134,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
126
134
  ok: false,
127
135
  code: "DEVICE_NAME_AMBIGUOUS",
128
136
  message: `device name '${device}' is ambiguous; use the id:
129
- ` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n")
137
+ ` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n"),
138
+ userAction: deviceRecovery(options, opts.rerun)
130
139
  };
131
140
  }
132
141
  return {
@@ -134,7 +143,8 @@ function applyDeviceTargeting(command, adapter, options, opts) {
134
143
  code: "DEVICE_NOT_FOUND",
135
144
  message: `--device ${device} did not match any connected device.
136
145
  ` + (connected2.length > 0 ? ` Connected devices:
137
- ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
146
+ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`),
147
+ userAction: deviceRecovery(options, opts.rerun)
138
148
  };
139
149
  }
140
150
  const connected = listConnectedDevices();
@@ -158,7 +168,8 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
158
168
  message: `--platform ${platformPreference} conflicts with the current slot-selected device.
159
169
  Selected devices:
160
170
  ${formatConnectedDevices(selectedTargetable2)}
161
- To use a different connected device intentionally, pass --device <id>.`
171
+ To use a different connected device intentionally, pass --device <id>.`,
172
+ userAction: deviceRecovery(options, opts.rerun)
162
173
  };
163
174
  }
164
175
  if (!configuredPin && oppositePin) {
@@ -167,7 +178,8 @@ ${formatConnectedDevices(selectedTargetable2)}
167
178
  code: "DEVICE_PLATFORM_CONFLICT",
168
179
  message: `--platform ${platformPreference} conflicts with the configured slot target.
169
180
  Configured ${oppositePlatform(platformPreference)} target: ${oppositePin}
170
- To use a different connected device intentionally, pass --device <id>.`
181
+ To use a different connected device intentionally, pass --device <id>.`,
182
+ userAction: deviceRecovery(options, opts.rerun)
171
183
  };
172
184
  }
173
185
  if (configuredPin) {
@@ -178,8 +190,8 @@ ${formatConnectedDevices(selectedTargetable2)}
178
190
  message: `--platform ${platformPreference} matches the configured slot target, but that target is not ready.
179
191
  Configured target: ${configuredPin}
180
192
  Connected devices:
181
- ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}
182
- Next: run mm-harness launch ${platformPreference}, or pass --device <id> to intentionally use another target.`
193
+ ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}`,
194
+ userAction: `mm-harness launch ${platformPreference} --target ${shellQuoteArg(targetPath(options))}`
183
195
  };
184
196
  }
185
197
  if (platformTargetable.length === 1) {
@@ -195,7 +207,8 @@ ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}
195
207
  code: "DEVICE_PLATFORM_NOT_FOUND",
196
208
  message: `--platform ${platformPreference} did not match any ready connected device.
197
209
  ` + (connected.length > 0 ? ` Connected devices:
198
- ${formatConnectedDevices(connected)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
210
+ ${formatConnectedDevices(connected)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`),
211
+ userAction: deviceRecovery(options, opts.rerun)
199
212
  };
200
213
  }
201
214
  return {
@@ -205,7 +218,8 @@ ${formatConnectedDevices(connected)}` : ` No devices are currently connected (a
205
218
  Connected devices:
206
219
  ${formatConnectedDevices(connected)}
207
220
  Add --device <id> to disambiguate, e.g.:${platformTargetable.map((d) => `
208
- --device ${d.id}`).join("")}`
221
+ --device ${d.id}`).join("")}`,
222
+ userAction: deviceRecovery(options, opts.rerun)
209
223
  };
210
224
  }
211
225
  const selectedTargetable = targetable.filter(deviceSelected);
@@ -224,7 +238,8 @@ ${formatConnectedDevices(connected)}
224
238
  Connected devices:
225
239
  ${formatConnectedDevices(connected)}
226
240
  ` + (selectedTargetable.length > 1 ? ` The current slot context selects more than one target; fix the slot device pins or add --device <id>.` : ` Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
227
- --device ${d.id}`).join("")}`)
241
+ --device ${d.id}`).join("")}`),
242
+ userAction: deviceRecovery(options, opts.rerun)
228
243
  };
229
244
  }
230
245
  return { ok: true };