@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
package/dist/adapters.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import http from "node:http";
2
2
  import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
3
- import { runLiveAdapterScript } from "./live-adapter-contract.js";
3
+ import {
4
+ runLiveAdapterScript
5
+ } from "./live-adapter-contract.js";
4
6
  import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
5
7
  import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
6
8
  function sleep(ms) {
@@ -32,6 +34,7 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
32
34
  "metamask.wallet.setup",
33
35
  "metamask.wallet.ensure_unlocked",
34
36
  "metamask.wallet.select_account",
37
+ "metamask.wallet.list_accounts",
35
38
  "metamask.wallet.read_state"
36
39
  ]);
37
40
  const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set(["ui.navigate"]);
@@ -42,14 +45,20 @@ function liveRuntimeConfigured(platform, node) {
42
45
  if (platform === "mobile" || platform === "core") return true;
43
46
  return Boolean(node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
44
47
  }
45
- async function runLiveFirst(platform, action, node, context) {
48
+ async function runLiveFirst(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
46
49
  if (node.allow_static_placeholder === true) {
47
50
  throw new Error(
48
51
  `${action} refused allow_static_placeholder. MetaMask runner proof runs must use live adapters and real artifacts.`
49
52
  );
50
53
  }
51
- if (!requiresLiveAdapter(platform, action) && !liveRuntimeConfigured(platform, node)) return null;
52
- const live = await runLiveAdapterScript({ platform, action, node, context });
54
+ if (!forceLive && !requiresLiveAdapter(platform, action) && !liveRuntimeConfigured(platform, node)) return null;
55
+ const live = await runLiveAdapterScript({
56
+ platform,
57
+ action,
58
+ node,
59
+ context,
60
+ prepared: preparedLiveAdapters?.get(action)
61
+ });
53
62
  if (!live) return null;
54
63
  const liveResult = isRecord(live.result) ? live.result : { result: live.result };
55
64
  const output = { ...liveResult, liveAdapter: live.script };
@@ -64,10 +73,17 @@ function liveAdapterPathHint(platform, action) {
64
73
  }
65
74
  return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
66
75
  }
67
- async function semanticResult(platform, action, node, context) {
68
- const live = await runLiveFirst(platform, action, node, context);
76
+ async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
77
+ const live = await runLiveFirst(
78
+ platform,
79
+ action,
80
+ node,
81
+ context,
82
+ forceLive,
83
+ preparedLiveAdapters
84
+ );
69
85
  if (live) return live;
70
- if (requiresLiveAdapter(platform, action)) {
86
+ if (forceLive || requiresLiveAdapter(platform, action)) {
71
87
  const expected = liveAdapterPathHint(platform, action);
72
88
  throw new Error(
73
89
  `${action} requires a live ${platform} adapter that drives a real supported app/API path; no adapter script was found or no live runtime is configured (for example ${expected}). Static placeholders are refused to avoid fabricated proof. Set METAMASK_RECIPE_LIVE_ADAPTER_DIR or add a runner library/actions script.`
@@ -145,15 +161,16 @@ function probeHttpJson(url, timeoutMs = 1e3) {
145
161
  });
146
162
  });
147
163
  }
148
- function createMetaMaskSemanticAdapters(platform) {
164
+ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], preparedLiveAdapters) {
149
165
  const walletActions = [
150
166
  "metamask.wallet.fixture_status",
151
167
  "metamask.wallet.setup",
152
168
  "metamask.wallet.ensure_unlocked",
153
169
  "metamask.wallet.select_account",
170
+ "metamask.wallet.list_accounts",
154
171
  "metamask.wallet.read_state"
155
172
  ];
156
- const actions = [
173
+ const bundledActions = [
157
174
  ...walletActions,
158
175
  "metamask.perps.read_positions",
159
176
  "metamask.perps.ensure_positions",
@@ -169,10 +186,19 @@ function createMetaMaskSemanticAdapters(platform) {
169
186
  // read_account is core-only: only the headless core adapter implements it.
170
187
  ...platform === "core" ? ["metamask.perps.read_account"] : []
171
188
  ];
189
+ const bundled = new Set(bundledActions);
190
+ const actions = [.../* @__PURE__ */ new Set([...bundledActions, ...declaredCustomActions])];
172
191
  return actions.map(
173
192
  (action) => simpleAdapter(
174
193
  action,
175
- async (node, context) => semanticResult(platform, action, node, context)
194
+ async (node, context) => semanticResult(
195
+ platform,
196
+ action,
197
+ node,
198
+ context,
199
+ !bundled.has(action),
200
+ preparedLiveAdapters
201
+ )
176
202
  )
177
203
  );
178
204
  }
@@ -439,7 +465,7 @@ function mobileHudStatusLabel(status) {
439
465
  if (status === "pass") return "pass";
440
466
  return "run";
441
467
  }
442
- function createMetaMaskUiTransport(platform, harness) {
468
+ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
443
469
  if (platform === "core") {
444
470
  return {
445
471
  async execute(action) {
@@ -468,19 +494,26 @@ function createMetaMaskUiTransport(platform, harness) {
468
494
  return {
469
495
  async execute(action, node, context) {
470
496
  if (action === "ui.navigate") {
471
- const live = await runLiveFirst(platform, action, node, context);
497
+ const live = await runLiveFirst(
498
+ platform,
499
+ action,
500
+ node,
501
+ context,
502
+ false,
503
+ preparedLiveAdapters
504
+ );
472
505
  if (live) return live.output;
473
506
  throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
474
507
  }
475
- return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node, platform) : node, context);
508
+ return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
476
509
  }
477
510
  };
478
511
  }
479
- function normalizeUiWaitNode(node, adapter) {
480
- if (adapter !== "extension" || node.expected !== void 0 || typeof node.visible !== "boolean") return node;
512
+ function normalizeUiWaitNode(node) {
513
+ if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
481
514
  return { ...node, expected: node.visible ? "visible" : "absent" };
482
515
  }
483
- function createMetaMaskAdapters(adapter) {
516
+ function createMetaMaskAdapters(adapter, declaredCustomActions = [], preparedLiveAdapters) {
484
517
  const platform = adapter;
485
518
  const adapters = [
486
519
  simpleAdapter("app.status", async (_node, context) => ({
@@ -532,7 +565,7 @@ function createMetaMaskAdapters(adapter) {
532
565
  }
533
566
  };
534
567
  }),
535
- ...createMetaMaskSemanticAdapters(platform)
568
+ ...createMetaMaskSemanticAdapters(platform, declaredCustomActions, preparedLiveAdapters)
536
569
  ];
537
570
  return adapters;
538
571
  }
@@ -7,7 +7,7 @@ function acquireCheckoutLock(target, operation) {
7
7
  const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
8
8
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
9
9
  const inheritedToken = process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
10
- for (let attempt = 0; attempt < 2; attempt += 1) {
10
+ for (let attempt = 0; attempt < 4; attempt += 1) {
11
11
  try {
12
12
  const fd = fs.openSync(lockPath, "wx", 384);
13
13
  const token = inheritedToken || randomUUID();
@@ -46,11 +46,36 @@ function acquireCheckoutLock(target, operation) {
46
46
  message: `checkout sandbox is busy with ${String(owner.operation ?? "another operation")} (pid ${owner.pid}).`
47
47
  };
48
48
  }
49
- fs.rmSync(lockPath, { force: true });
49
+ claimStaleLock(lockPath, owner);
50
50
  }
51
51
  }
52
52
  return { path: lockPath, owner: readOwner(lockPath), message: "checkout sandbox lock could not be acquired." };
53
53
  }
54
+ function claimStaleLock(lockPath, expectedOwner) {
55
+ const claimedPath = `${lockPath}.stale-${process.pid}-${randomUUID()}`;
56
+ try {
57
+ fs.renameSync(lockPath, claimedPath);
58
+ } catch (error) {
59
+ if (error.code === "ENOENT") return;
60
+ throw error;
61
+ }
62
+ try {
63
+ const claimedOwner = readOwner(claimedPath);
64
+ if (!sameOwner(claimedOwner, expectedOwner) || claimedOwner && typeof claimedOwner.pid === "number" && processAlive(claimedOwner.pid)) {
65
+ try {
66
+ fs.linkSync(claimedPath, lockPath);
67
+ } catch (error) {
68
+ if (error.code !== "EEXIST") throw error;
69
+ }
70
+ }
71
+ } finally {
72
+ fs.rmSync(claimedPath, { force: true });
73
+ }
74
+ }
75
+ function sameOwner(left, right) {
76
+ if (!left || !right) return left === right;
77
+ return left.pid === right.pid && left.token === right.token && left.startedAt === right.startedAt && left.repoRoot === right.repoRoot;
78
+ }
54
79
  function readOwner(lockPath) {
55
80
  try {
56
81
  const value = JSON.parse(fs.readFileSync(lockPath, "utf8"));
package/dist/cli-color.js CHANGED
@@ -47,6 +47,24 @@ function colorStatusWord(word, { stream = process.stderr } = {}) {
47
47
  }
48
48
  return color("info", word, { stream });
49
49
  }
50
+ function colorHumanMessage(message, { stream = process.stderr } = {}) {
51
+ return String(message).split("\n").map((line) => {
52
+ const marker = /^(\s*)(→|✓|✗)\s?(.*)$/u.exec(line);
53
+ if (marker) {
54
+ const style = marker[2] === "\u2713" ? "ok" : marker[2] === "\u2717" ? "err" : "accent";
55
+ return `${marker[1]}${color(style, marker[2], { stream })}${marker[3] ? ` ${marker[3]}` : ""}`;
56
+ }
57
+ const next = /^(\s*)Next:\s*(.*)$/u.exec(line);
58
+ if (next) {
59
+ return `${next[1]}${color("label", "Next:", { stream })}${next[2] ? ` ${color("cmd", next[2], { stream })}` : ""}`;
60
+ }
61
+ const warning = /^(\s*)(WARN(?:ING)?)(:?)[ \t]*(.*)$/iu.exec(line);
62
+ if (warning) {
63
+ return `${warning[1]}${color("warn", `${warning[2]}${warning[3]}`, { stream })}${warning[4] ? ` ${warning[4]}` : ""}`;
64
+ }
65
+ return line;
66
+ }).join("\n");
67
+ }
50
68
  function classifyLogEvent(event) {
51
69
  const text = String(event || "");
52
70
  if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
@@ -72,6 +90,7 @@ export {
72
90
  classifyLogEvent,
73
91
  color,
74
92
  colorEnabled,
93
+ colorHumanMessage,
75
94
  colorKv,
76
95
  colorLogEvent,
77
96
  colorStatusWord,
@@ -15,7 +15,7 @@ const SPEC = {
15
15
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
16
16
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
17
17
  { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
18
- { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action"] },
18
+ { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action", "--library"] },
19
19
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
20
20
  { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
21
21
  { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
package/dist/cli.js CHANGED
@@ -23,7 +23,6 @@ import { handleCheck } from "./commands/check.js";
23
23
  import { handleChecklist } from "./commands/checklist.js";
24
24
  import { handleLast } from "./commands/last.js";
25
25
  import { parseArgs, targetPath } from "./commands/parse-args.js";
26
- import { runOneNode } from "./commands/run-engine.js";
27
26
  const COMMANDS = {
28
27
  actions: handleActions,
29
28
  doctor: handleDoctor,
@@ -81,7 +80,7 @@ ONE bin: mm-harness is the only command. No per-platform binaries \u2014 platfor
81
80
  auto-detected, the positional target forces it (mm-harness launch ios), and
82
81
  platform-specific needs are FLAGS on the same command (e.g. --sidebar, --full-build).
83
82
  Human happy path = the bare command; agents add depth via flags (--json, --target, ports).
84
- See README.md for the current workflow and docs/CHEATSHEET.md for examples.
83
+ See README.md for the workflow and docs/RECIPES.md for proof authoring.
85
84
  `);
86
85
  }
87
86
  async function main(argv) {
@@ -122,7 +121,7 @@ async function main(argv) {
122
121
  if (command === "stop") return handleStop(argv.slice(1));
123
122
  if (command === "logs") return handleLogs(argv.slice(1));
124
123
  if (command === "debug") return handleDebug(argv.slice(1));
125
- if (command === "fixtures") return handleFixtures(argv.slice(1), { runOneNode });
124
+ if (command === "fixtures") return handleFixtures(argv.slice(1));
126
125
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
127
126
  if (command === "check") return handleCheck(argv.slice(1));
128
127
  if (command === "checklist") return handleChecklist(argv.slice(1));
@@ -25,7 +25,12 @@ const RECIPE_RUNTIME = {
25
25
  "--validation-runtime-dir": value(),
26
26
  "--launch-existing-dist": bool(),
27
27
  "--record-video": optionalValue(["full-run", "off"]),
28
- "--record": bool()
28
+ "--record": bool(),
29
+ "--source-trust": value(["trusted", "untrusted", "unknown"]),
30
+ "--source-kind": value(["bundled", "operator", "task", "recipe-file", "uses-catalog", "library", "custom-adapter"]),
31
+ "--source-name": value(),
32
+ "--source-digest": value(),
33
+ "--approve-plan": value()
29
34
  };
30
35
  const RUNWAY = {
31
36
  "--branch": value(),
@@ -82,6 +87,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
82
87
  "--categories": bool(),
83
88
  "--category": value(),
84
89
  "--action-manifest": value(),
90
+ "--library": value(),
85
91
  "--raw": bool()
86
92
  }),
87
93
  positionals: [{ label: "query" }],
@@ -115,12 +121,16 @@ const PUBLIC_COMMAND_CONTRACTS = {
115
121
  "--run": value(),
116
122
  "--force": bool()
117
123
  }),
118
- positionals: [{ label: "action", choices: ["list", "promote"] }]
124
+ positionals: [
125
+ { label: "action", choices: ["list", "describe", "promote"] },
126
+ { label: "ref" }
127
+ ]
119
128
  },
120
129
  run: {
121
130
  options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
122
131
  "--list": bool(),
123
- "--plan": bool()
132
+ "--plan": bool(),
133
+ "--describe": bool()
124
134
  }),
125
135
  positionals: [{ label: "recipe" }],
126
136
  minimumPositionals: 1,
@@ -1,8 +1,7 @@
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";
@@ -28,12 +27,14 @@ import {
28
27
  prepareHeal,
29
28
  recoverRunInfra,
30
29
  resolveMetaMaskLibrarySources,
30
+ preflightRecipe,
31
31
  runRecipe,
32
32
  synthesizeOneNodeRecipe,
33
33
  validateRecipeAdapterAware
34
34
  } from "./run-engine.js";
35
35
  import { acquireCheckoutLock } from "../checkout-lock.js";
36
36
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
37
+ import { recipeTrustFailure } from "../recipe-security.js";
37
38
  async function handleCall(argv) {
38
39
  if (argv.includes("--list")) {
39
40
  const { options: options2 } = parseArgs(argv, "call");
@@ -52,7 +53,8 @@ async function handleCall(argv) {
52
53
  let discovery = "mm-harness actions";
53
54
  try {
54
55
  const { adapter: adapter2 } = resolveAdapter(options);
55
- 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);
56
58
  const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
57
59
  const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
58
60
  example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
@@ -101,7 +103,8 @@ async function handleCall(argv) {
101
103
  return EXIT.bounded;
102
104
  }
103
105
  const actionManifestOverride = optionString(options, "actionManifest");
104
- const manifest = loadActionManifest(adapter, actionManifestOverride);
106
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
107
+ const { manifest } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
105
108
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
106
109
  const names = getRecipeActionManifestActionNames(manifest);
107
110
  const resolution = resolveActionName(shortName, names);
@@ -137,7 +140,7 @@ async function handleCall(argv) {
137
140
  if (validation.status === "invalid") {
138
141
  const message = `call ${resolvedAction}: recipe validation failed`;
139
142
  const userAction = `mm-harness actions --action ${resolvedAction} --adapter ${adapter} --json`;
140
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
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));
141
144
  else {
142
145
  console.error(`\u2717 ${message}`);
143
146
  for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
@@ -145,6 +148,61 @@ async function handleCall(argv) {
145
148
  }
146
149
  return EXIT.validation;
147
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}`);
203
+ }
204
+ return EXIT.validation;
205
+ }
148
206
  const lock = acquireCheckoutLock(target, "call");
149
207
  if ("message" in lock) {
150
208
  return checkoutBusyOut(json, "call", lock.message, lock.path);
@@ -155,19 +213,20 @@ async function handleCall(argv) {
155
213
  });
156
214
  if (typeof prepared === "number") return prepared;
157
215
  const { state, heal } = prepared;
158
- const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
159
- const recipeFile = path.join(scratch, "call.recipe.json");
160
- fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
161
- `);
162
- const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
163
- const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
164
- const callRuntimeOptions = {
165
- ...runtimeOptionsFromCli(options),
166
- ...librarySources ? { librarySources } : {},
167
- stdoutIsMachineContract: json
168
- };
169
216
  const { result, violation } = await executeWithHealBounds(
170
- () => 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
+ },
171
230
  adapter,
172
231
  target,
173
232
  heal,
@@ -176,6 +235,8 @@ async function handleCall(argv) {
176
235
  );
177
236
  if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
178
237
  const callOutput = readCallOutput(result.tracePath);
238
+ const safeArgs = redactCallValue(args);
239
+ const safeCallOutput = redactCallValue(callOutput);
179
240
  const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
180
241
  if (json) {
181
242
  console.log(
@@ -186,14 +247,14 @@ async function handleCall(argv) {
186
247
  adapter,
187
248
  action: shortName,
188
249
  resolvedAction,
189
- args,
250
+ args: safeArgs,
190
251
  status: result.status,
191
252
  summaryPath: result.summaryPath,
192
253
  tracePath: result.tracePath,
193
254
  artifactManifestPath: result.artifactManifestPath,
194
255
  ...result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {},
195
256
  ...result.sideFindings ? { sideFindings: result.sideFindings } : {},
196
- ...callOutput !== void 0 ? { output: callOutput } : {},
257
+ ...callOutput !== void 0 ? { output: safeCallOutput } : {},
197
258
  recovered: state.recovered,
198
259
  mutations: state.mutations,
199
260
  exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime,
@@ -204,9 +265,7 @@ async function handleCall(argv) {
204
265
  )
205
266
  );
206
267
  } else {
207
- const rendered = callOutput !== void 0 ? `
208
- Result:
209
- ${formatCallOutput(callOutput)}` : "";
268
+ const renderedInputs = Object.keys(args).length > 0 ? formatCallOutput(safeArgs) : "";
210
269
  const out = (style, text) => color(style, text, { stream: process.stdout });
211
270
  const diagnostics = formatRunDiagnosticsForHuman(
212
271
  readRunDiagnosticsDocument(result.diagnosticsPath),
@@ -216,9 +275,11 @@ ${formatCallOutput(callOutput)}` : "";
216
275
  ${diagnostics.map((line) => ` ${line}`).join("\n")}
217
276
  `;
218
277
  console.log(
219
- `${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 ? `
220
281
  ${out("label", "Result:")}
221
- ${formatCallOutput(callOutput)}` : ""}
282
+ ${formatCallOutput(safeCallOutput)}` : ""}
222
283
  ` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
223
284
  Next: ${failureUserAction}` : "")
224
285
  );
@@ -228,6 +289,11 @@ ${formatCallOutput(callOutput)}` : ""}
228
289
  lock.release();
229
290
  }
230
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
+ }
231
297
  function readCallOutput(tracePath) {
232
298
  try {
233
299
  const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
@@ -243,20 +309,32 @@ function formatCallOutput(value) {
243
309
  if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
244
310
  return JSON.stringify(value, null, 2);
245
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
+ }
246
320
  async function handleCallHelp(argv, genericHelp) {
247
321
  const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
248
322
  const { options } = parseArgs(rest, "call");
249
323
  let adapter;
250
324
  let manifest;
325
+ let actionSources = /* @__PURE__ */ new Map();
251
326
  try {
252
327
  ({ adapter } = resolveAdapter(options));
253
- 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;
254
332
  } catch {
255
333
  process.stdout.write(`${genericHelp}
256
334
  `);
257
335
  return EXIT.ok;
258
336
  }
259
- const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest), shortName) : [];
337
+ const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest, actionSources), shortName) : [];
260
338
  if (matches.length === 0) {
261
339
  process.stdout.write(`${genericHelp}
262
340
  `);
@@ -284,7 +362,8 @@ function renderCallActionHelp(entry) {
284
362
  );
285
363
  const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
286
364
  if (entry.description) lines.push(` ${entry.description}`, "");
287
- 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();
288
367
  if (names.length === 0) {
289
368
  lines.push(" Fields: (none)");
290
369
  } else {
@@ -374,6 +453,7 @@ function isForwardedOptionValue(argv, index) {
374
453
  }
375
454
  const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
376
455
  "action-manifest",
456
+ "approve-plan",
377
457
  "adapter",
378
458
  "artifacts-dir",
379
459
  "cdp-port",
@@ -384,6 +464,10 @@ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
384
464
  "platform",
385
465
  "project-root",
386
466
  "slot",
467
+ "source-digest",
468
+ "source-kind",
469
+ "source-name",
470
+ "source-trust",
387
471
  "target",
388
472
  "validation-runtime-dir",
389
473
  "watcher-port"
@@ -407,6 +491,8 @@ function pickCallExampleAction(names) {
407
491
  return names[0] ?? "command";
408
492
  }
409
493
  export {
494
+ defaultCallArtifactsDir,
410
495
  handleCall,
411
- handleCallHelp
496
+ handleCallHelp,
497
+ redactCallValue
412
498
  };
@@ -4,6 +4,9 @@ import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import { EXIT } from "./shared.js";
6
6
  const require2 = createRequire(import.meta.url);
7
+ const MARK_SCRIPT = require2.resolve(
8
+ "@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
9
+ );
7
10
  const USAGE = "mm-harness checklist mark <task-dir> <step> [options]";
8
11
  function terminalSignalStatus(taskDir) {
9
12
  try {
@@ -122,7 +125,7 @@ async function handleChecklist(argv) {
122
125
  teachRecipeCompletion(resolvedTaskDir, markArgs);
123
126
  const result = spawnSync(
124
127
  process.execPath,
125
- [require2.resolve("@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"), resolvedTaskDir, ...markArgs],
128
+ [MARK_SCRIPT, resolvedTaskDir, ...markArgs],
126
129
  { stdio: "inherit" }
127
130
  );
128
131
  if (result.error) {