@deeeed/metamask-harness 0.80.0 → 0.82.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 (118) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/adapters/manifest.json +48 -64
  3. package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
  4. package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
  5. package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
  6. package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
  7. package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
  8. package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
  9. package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
  10. package/adapters/web-dapp/lib/policy.mjs +79 -0
  11. package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
  12. package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
  16. package/dist/adapters/core/actions.js +4 -1
  17. package/dist/adapters/extension/actions.js +6 -2
  18. package/dist/adapters/extension/network-observer.js +13 -0
  19. package/dist/adapters/extension/run.js +115 -31
  20. package/dist/adapters/mobile/actions.js +4 -1
  21. package/dist/adapters/mobile/network-observer.js +68 -0
  22. package/dist/adapters/mobile/performance-observer.js +1 -3
  23. package/dist/adapters/mobile/run.js +32 -9
  24. package/dist/adapters/perps-order-inputs.js +36 -0
  25. package/dist/adapters/plugin-surface.js +98 -0
  26. package/dist/adapters/proof-failure.js +11 -0
  27. package/dist/adapters/surface.js +1 -1
  28. package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
  29. package/dist/adapters/web-dapp/console-capture.js +63 -0
  30. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  31. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
  32. package/dist/adapters/web-dapp/overlay.js +21 -0
  33. package/dist/adapters/web-dapp/readiness.js +10 -0
  34. package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
  35. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  36. package/dist/adapters/web-dapp/surface.js +149 -0
  37. package/dist/adapters.js +20 -4
  38. package/dist/cli.js +34 -6
  39. package/dist/command-contract.js +29 -7
  40. package/dist/commands/check.js +32 -8
  41. package/dist/commands/completion-candidates.js +5 -5
  42. package/dist/commands/doctor.js +19 -2
  43. package/dist/commands/farmslot-ready.js +1 -0
  44. package/dist/commands/help.js +1 -1
  45. package/dist/commands/launch/device.js +5 -5
  46. package/dist/commands/self-test.js +10 -5
  47. package/dist/engine.js +47 -0
  48. package/dist/funding-execution-context.js +81 -0
  49. package/dist/manifest.js +48 -24
  50. package/dist/mm-harness-cli.js +20 -3
  51. package/dist/performance-observation.js +7 -15
  52. package/dist/recipe-assistance-context.js +6 -7
  53. package/dist/runner.js +28 -8
  54. package/docs/CONTRIBUTING.md +4 -2
  55. package/docs/RECIPES.md +31 -3
  56. package/docs/cli-contract.md +5 -3
  57. package/library/actions/core/perps/_controller.mjs +24 -28
  58. package/library/actions/core/perps/_rewards.mjs +186 -0
  59. package/library/actions/core/perps/close_positions.mjs +1 -1
  60. package/library/actions/core/perps/place_order.mjs +167 -45
  61. package/library/actions/core/perps/quote_fees.mjs +7 -4
  62. package/library/actions/core/wallet/list_accounts.mjs +1 -1
  63. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  64. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  65. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  66. package/library/actions/extension/perps/perps.mjs +320 -3
  67. package/library/actions/extension/perps/place_order.mjs +3 -1
  68. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  69. package/library/actions/extension/platform/cdp.mjs +71 -3
  70. package/library/actions/mobile/perps/perps.mjs +216 -11
  71. package/library/actions/mobile/perps/place_order.mjs +3 -1
  72. package/library/actions/shared/console-collector.mjs +1 -1
  73. package/library/actions/shared/perps/order-policy.mjs +177 -0
  74. package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
  75. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  76. package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
  77. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
  78. package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
  79. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  80. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  81. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  82. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
  83. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  84. package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
  85. package/library/manifests/core.action-manifest.json +6 -2
  86. package/library/manifests/extension.action-manifest.json +4 -0
  87. package/library/manifests/mobile.action-manifest.json +12 -0
  88. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  89. package/package.json +6 -6
  90. package/site/assets/help-recipes.json +1 -0
  91. package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
  92. package/adapters/terminal/lib/origin.mjs +0 -30
  93. package/adapters/terminal/lib/page-script.mjs +0 -246
  94. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  95. package/dist/adapters/terminal/console-capture.js +0 -59
  96. package/dist/adapters/terminal/doctor-checks.js +0 -22
  97. package/dist/adapters/terminal/overlay.js +0 -18
  98. package/dist/adapters/terminal/readiness.js +0 -11
  99. package/dist/adapters/terminal/surface.js +0 -133
  100. package/dist/commands/call.js +0 -671
  101. package/dist/commands/list-executables.js +0 -245
  102. package/dist/commands/manifest.js +0 -477
  103. package/dist/commands/run-engine.js +0 -1301
  104. package/dist/commands/run.js +0 -730
  105. package/dist/discovery.js +0 -15
  106. package/dist/metamask-action-validation.js +0 -68
  107. package/dist/network-observation.js +0 -290
  108. package/library/actions/terminal/perps/_venue.mjs +0 -242
  109. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  110. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  111. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  112. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  113. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  114. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  115. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  116. package/library/actions/terminal/ui/navigate.mjs +0 -31
  117. package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
  118. package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
package/dist/engine.js ADDED
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import { getAdapterSurface } from "./adapters/surface.js";
3
+ import { consoleClassifier } from "./console-classifier.js";
4
+ import {
5
+ authorizeTrustedMutationPlan,
6
+ fixturePolicyMutationBase,
7
+ fundingOptionsFromCli,
8
+ isFixturePolicyBase,
9
+ loadTrustedMutationBase
10
+ } from "./funding-execution-context.js";
11
+ import { resolveActionManifest, validateManifest } from "./manifest.js";
12
+ import { LIBRARY_ACTION_NAMESPACE, runnerDir } from "./paths.js";
13
+ import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
14
+ const metaMaskEngine = {
15
+ bundledLibrary: {
16
+ name: "metamask",
17
+ root: path.resolve(runnerDir, "library"),
18
+ actionNamespace: LIBRARY_ACTION_NAMESPACE
19
+ },
20
+ resolveActionManifest,
21
+ validateManifest,
22
+ actionCapabilities: metaMaskActionExecutionCapabilities,
23
+ createRunner: async (adapter, manifest, options) => {
24
+ const { createMetaMaskRunner } = await import("./runner.js");
25
+ return createMetaMaskRunner(adapter, manifest, options);
26
+ },
27
+ // Funding files from the run's command line, bound to the run's plan. Without
28
+ // them, a platform whose actions take a trusted mutation (the Extension) runs a
29
+ // plan with external mutations in the fixture-policy lane, where each action
30
+ // applies the shared order policy; any other plan runs without one.
31
+ trustedMutation: {
32
+ load: async ({ cli, adapter, ...run }) => await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0),
33
+ // Nothing to bind (undefined): recipe-cli keeps the runner it planned with.
34
+ authorize: async (base, plan) => {
35
+ if (!base) return void 0;
36
+ if (isFixturePolicyBase(base) && !plan.nodes.some((node) => node.capabilities.includes("external-mutation"))) {
37
+ return void 0;
38
+ }
39
+ return authorizeTrustedMutationPlan(base, plan);
40
+ }
41
+ },
42
+ console: consoleClassifier,
43
+ runnerIncludes: ["src", "adapters", "library", "bin", "dist", "package.json"]
44
+ };
45
+ export {
46
+ metaMaskEngine
47
+ };
@@ -35,6 +35,7 @@ const CLAIM_DIFF_FIELDS = [
35
35
  "operationCounts"
36
36
  ];
37
37
  const trustedActionContexts = /* @__PURE__ */ new WeakSet();
38
+ const fixturePolicySources = /* @__PURE__ */ new WeakMap();
38
39
  const fundingSources = /* @__PURE__ */ new WeakMap();
39
40
  const CURRENT_FUNDING_REGISTRY_SHA256 = "d4416e0e8a812a0e2f1798ae6f901a5820f7ee0b9783cfa5d515d0c7863ea80e";
40
41
  const PREVIOUS_FUNDING_REGISTRY_SHA256 = "7e79ee2a5f7bdde0a5548208cb2eab55881575cc7d826ee5f7acdbf15d49609c";
@@ -141,7 +142,70 @@ async function loadTrustedMutationBase(options) {
141
142
  });
142
143
  return trusted;
143
144
  }
145
+ const FIXTURE_POLICY_LANE = "fixture-policy";
146
+ const fixturePolicyMarker = (field) => sha256(`${FIXTURE_POLICY_LANE}:${field}`);
147
+ function fixturePolicyMutationBase(options) {
148
+ if (!path.isAbsolute(options.artifactsDir)) {
149
+ throw new Error("Runner artifactsDir must be absolute before a mutation context is created.");
150
+ }
151
+ const rootRecipeSha256 = normalizeRecipeDigest(digestRecipeDocument(options.rootRecipe));
152
+ const base = deepFreeze({
153
+ runId: sha256(canonical({ lane: FIXTURE_POLICY_LANE, artifactsDir: options.artifactsDir, rootRecipeSha256 })),
154
+ rootRecipeSha256,
155
+ requestSha256: fixturePolicyMarker("request"),
156
+ preflightSha256: fixturePolicyMarker("preflight"),
157
+ releaseProofSha256: fixturePolicyMarker("release-proof"),
158
+ balanceProofSha256: fixturePolicyMarker("balance-proof"),
159
+ fundingRequirementsSha256: fixturePolicyMarker("requirements"),
160
+ runtimeArtifactSha256: fixturePolicyMarker("runtime-artifact"),
161
+ runtimeTreeSha256: fixturePolicyMarker("runtime-tree"),
162
+ registrySha256: fixturePolicyMarker("registry"),
163
+ profileId: FIXTURE_POLICY_LANE,
164
+ domain: "perps",
165
+ client: "extension",
166
+ proofLane: FIXTURE_POLICY_LANE,
167
+ targetId: FIXTURE_POLICY_LANE,
168
+ subjectDigest: fixturePolicyMarker("subject"),
169
+ selectedIdentitySha256: "",
170
+ runtimeSessionId: "",
171
+ cdpPort: 0,
172
+ cdpTargetId: "",
173
+ extensionOrigin: "",
174
+ operations: [],
175
+ budgets: [],
176
+ requirements: []
177
+ });
178
+ fixturePolicySources.set(base, { artifactsDir: options.artifactsDir });
179
+ return base;
180
+ }
181
+ function isFixturePolicyBase(base) {
182
+ return base?.proofLane === FIXTURE_POLICY_LANE;
183
+ }
184
+ async function authorizeFixturePolicyPlan(base, plan) {
185
+ const source = fixturePolicySources.get(base);
186
+ if (!source) throw new Error("Fixture-policy plan authorization requires the runner-created context.");
187
+ const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
188
+ await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
189
+ const artifactsDir = await realpath(source.artifactsDir);
190
+ const artifactsIdentitySha256 = await directoryIdentity(artifactsDir);
191
+ const executionLedgerRoot = path.join(artifactsDir, "mutation-ledger");
192
+ await ensurePrivateDirectory(artifactsDir, executionLedgerRoot);
193
+ const runId = sha256(canonical({
194
+ lane: FIXTURE_POLICY_LANE,
195
+ rootRecipeSha256: base.rootRecipeSha256,
196
+ executionPlanSha256,
197
+ artifactsIdentitySha256
198
+ }));
199
+ const authorized = deepFreeze({ ...base, runId, executionPlanSha256, artifactsIdentitySha256 });
200
+ fixturePolicySources.set(authorized, {
201
+ ...source,
202
+ executionLedgerRoot,
203
+ executionClaimSha256: sha256(canonical({ kind: "fixture-policy-execution-claim", runId, executionPlanSha256 }))
204
+ });
205
+ return authorized;
206
+ }
144
207
  async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
208
+ if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, plan);
145
209
  const source = fundingSources.get(base);
146
210
  if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
147
211
  const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
@@ -228,6 +292,20 @@ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
228
292
  return authorized;
229
293
  }
230
294
  async function trustedMutationForAction(base, recipe, selectedIdentitySha256) {
295
+ if (isFixturePolicyBase(base)) {
296
+ const source2 = fixturePolicySources.get(base);
297
+ if (!source2?.executionLedgerRoot || !source2.executionClaimSha256) {
298
+ throw new Error("External mutation requires an authorized run-owned execution ledger.");
299
+ }
300
+ const trusted2 = deepFreeze({
301
+ ...base,
302
+ recipeSha256: normalizeRecipeDigest(digestRecipeDocument(recipe)),
303
+ executionLedgerRoot: source2.executionLedgerRoot,
304
+ executionClaimSha256: source2.executionClaimSha256
305
+ });
306
+ trustedActionContexts.add(trusted2);
307
+ return trusted2;
308
+ }
231
309
  if (!selectedIdentitySha256 || !SHA256.test(selectedIdentitySha256)) {
232
310
  throw new Error(
233
311
  "External mutation requires a successful same-run metamask.wallet.import selected identity proof. Next: run metamask.wallet.import before the mutation node."
@@ -1442,8 +1520,11 @@ function deepFreeze(value) {
1442
1520
  return value;
1443
1521
  }
1444
1522
  export {
1523
+ FIXTURE_POLICY_LANE,
1445
1524
  authorizeTrustedMutationPlan,
1525
+ fixturePolicyMutationBase,
1446
1526
  fundingOptionsFromCli,
1527
+ isFixturePolicyBase,
1447
1528
  loadTrustedMutationBase,
1448
1529
  selectedIdentityFromWalletImport,
1449
1530
  serializeTrustedMutationContext,
package/dist/manifest.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
4
4
  import { getAdapterSurface } from "./adapters/surface.js";
5
- import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
5
+ import { manifestPath, readJson, importRecipeProtocol, runnerDir } from "./paths.js";
6
6
  function loadMetaMaskMobileActionManifest() {
7
7
  return asActionManifest(readJson(manifestPath("mobile")));
8
8
  }
@@ -12,14 +12,26 @@ function loadMetaMaskExtensionActionManifest() {
12
12
  function loadMetaMaskCoreActionManifest() {
13
13
  return asActionManifest(readJson(manifestPath("core")));
14
14
  }
15
- function loadMetaMaskTerminalActionManifest() {
16
- return asActionManifest(readJson(manifestPath("terminal")));
17
- }
18
15
  function loadActionManifest(adapter, overridePath) {
19
16
  if (overridePath) return asActionManifest(readJson(path.resolve(overridePath)));
20
17
  return asActionManifest(readJson(getAdapterSurface(adapter).actions.manifestPath()));
21
18
  }
19
+ class ActionManifestError extends Error {
20
+ code = "ACTION_MANIFEST_INVALID";
21
+ userAction;
22
+ constructor(adapter, cause) {
23
+ super(cause instanceof Error ? cause.message : String(cause));
24
+ this.userAction = `fix the ${adapter} action manifest the message names, then rerun`;
25
+ }
26
+ }
22
27
  async function resolveActionManifest(adapter, overridePath, librarySources, taskActionRoot) {
28
+ try {
29
+ return await resolveActionManifestSources(adapter, overridePath, librarySources, taskActionRoot);
30
+ } catch (error) {
31
+ throw error instanceof ActionManifestError ? error : new ActionManifestError(adapter, error);
32
+ }
33
+ }
34
+ async function resolveActionManifestSources(adapter, overridePath, librarySources, taskActionRoot) {
23
35
  if (overridePath) {
24
36
  const manifestPath2 = path.resolve(overridePath);
25
37
  const implementationRoot = path.resolve(
@@ -38,9 +50,12 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
38
50
  })
39
51
  };
40
52
  }
41
- const canonicalPath = path.resolve(getAdapterSurface(adapter).actions.manifestPath());
53
+ const surfaceActions = getAdapterSurface(adapter).actions;
54
+ const canonicalPath = path.resolve(surfaceActions.manifestPath());
42
55
  const canonicalRoot = path.dirname(path.dirname(canonicalPath));
43
- const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: canonicalRoot }];
56
+ const bundledRoot = path.resolve(runnerDir, "library");
57
+ const canonicalName = canonicalRoot === bundledRoot ? "metamask" : path.basename(canonicalRoot);
58
+ const sources = librarySources?.length ? librarySources : [{ name: canonicalName, root: canonicalRoot }];
44
59
  const manifests = [];
45
60
  for (const source of sources) {
46
61
  const file = path.resolve(source.root, "manifests", `${adapter}.action-manifest.json`);
@@ -49,31 +64,39 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
49
64
  await validateManifest(manifest);
50
65
  manifests.push({ source, file, manifest });
51
66
  }
67
+ for (const declared of surfaceActions.manifestPaths?.() ?? []) {
68
+ const file = path.resolve(declared);
69
+ if (file === canonicalPath || manifests.some((entry) => entry.file === file)) continue;
70
+ const manifest = asActionManifest(readJson(file));
71
+ await validateManifest(manifest);
72
+ const root = path.dirname(path.dirname(file));
73
+ const source = sources.find((candidate) => path.resolve(candidate.root) === root) ?? { name: path.basename(root), root };
74
+ manifests.push({ source, file, manifest });
75
+ }
52
76
  if (!manifests.some((entry) => entry.file === canonicalPath)) {
53
77
  const manifest = loadActionManifest(adapter);
54
78
  await validateManifest(manifest);
55
79
  manifests.push({
56
- source: { name: "metamask", root: canonicalRoot },
80
+ source: { name: canonicalName, root: canonicalRoot },
57
81
  file: canonicalPath,
58
82
  manifest
59
83
  });
60
84
  }
61
85
  const canonical = manifests.find((entry) => entry.file === canonicalPath);
62
86
  if (!canonical) throw new Error(`No ${adapter} action manifest could be resolved.`);
87
+ const isBundled = (entry) => path.resolve(entry.source.root) === bundledRoot;
88
+ const bundledParents = manifests.filter((entry) => entry !== canonical && isBundled(entry));
63
89
  const official = new Set(OFFICIAL_RECIPE_ACTIONS);
64
- const canonicalActions = actionsOf(canonical.manifest);
65
90
  const mergedActions = {};
66
- for (const [name, entry] of Object.entries(canonicalActions)) {
67
- if (official.has(name)) mergedActions[name] = entry;
68
- }
69
91
  const actionSources = /* @__PURE__ */ new Map();
70
- for (const name of Object.keys(mergedActions)) {
71
- actionSources.set(name, {
72
- name: "official",
73
- tier: "official",
74
- manifestPath: canonical.file
75
- });
92
+ for (const entry of [canonical, ...bundledParents]) {
93
+ for (const [name, action] of Object.entries(actionsOf(entry.manifest))) {
94
+ if (!official.has(name) || name in mergedActions) continue;
95
+ mergedActions[name] = action;
96
+ actionSources.set(name, { name: "official", tier: "official", manifestPath: entry.file });
97
+ }
76
98
  }
99
+ const observers = bundledParents.length ? [canonical, ...bundledParents].flatMap((entry) => entry.manifest.observers ?? []).filter((observer, index, all) => all.findIndex((other) => other.ref === observer.ref) === index) : canonical.manifest.observers;
77
100
  const custom = /* @__PURE__ */ new Set();
78
101
  const ordered = [
79
102
  ...manifests.filter((entry) => entry.file !== canonicalPath),
@@ -81,24 +104,25 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
81
104
  ];
82
105
  for (const entry of ordered) {
83
106
  const isCanonical = entry.file === canonicalPath;
84
- if (!isCanonical && entry.manifest.observers?.length) {
107
+ const bundled = isBundled(entry);
108
+ if (!isCanonical && !bundled && entry.manifest.observers?.length) {
85
109
  throw new Error(
86
110
  `Library manifest ${entry.file} declares observers, but external library observers are not executable; remove observers or use an explicit task action manifest.`
87
111
  );
88
112
  }
89
- const sourceName = isCanonical ? "metamask" : entry.source.name ?? path.basename(entry.source.root);
90
- const tier = isCanonical ? "canonical" : sourceName === "personal" ? "personal" : "team";
113
+ const sourceName = bundled ? "metamask" : entry.source.name ?? path.basename(entry.source.root);
114
+ const tier = bundled ? "canonical" : sourceName === "personal" ? "personal" : "team";
91
115
  const implementationRoot = path.join(entry.source.root, "actions");
92
116
  const sourceInfo = {
93
117
  name: sourceName,
94
118
  tier,
95
119
  manifestPath: entry.file,
96
120
  implementationRoot,
97
- trust: entry.source.provenance?.trust ?? (isCanonical ? "trusted" : "unknown")
121
+ trust: entry.source.provenance?.trust ?? (bundled ? "trusted" : "unknown")
98
122
  };
99
123
  for (const [name, action] of Object.entries(actionsOf(entry.manifest))) {
100
124
  if (official.has(name)) {
101
- if (!isCanonical) {
125
+ if (!isCanonical && !bundled) {
102
126
  throw new Error(
103
127
  `Library action ${name} from ${entry.file} conflicts with an official action; team libraries may declare namespaced custom actions only.`
104
128
  );
@@ -121,7 +145,7 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
121
145
  manifest: {
122
146
  $schema: canonical.manifest.$schema,
123
147
  actions: mergedActions,
124
- ...canonical.manifest.observers ? { observers: canonical.manifest.observers } : {}
148
+ ...observers ? { observers } : {}
125
149
  },
126
150
  actionSources
127
151
  };
@@ -163,11 +187,11 @@ function asActionManifest(value) {
163
187
  return value;
164
188
  }
165
189
  export {
190
+ ActionManifestError,
166
191
  loadActionManifest,
167
192
  loadMetaMaskCoreActionManifest,
168
193
  loadMetaMaskExtensionActionManifest,
169
194
  loadMetaMaskMobileActionManifest,
170
- loadMetaMaskTerminalActionManifest,
171
195
  resolveActionManifest,
172
196
  validateManifest
173
197
  };
@@ -4,14 +4,16 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { Command } from "commander";
7
- import { color, withCommandJournal, JsonStreamWriter, detectAdapter } from "@farmslot/recipe-cli/harness";
7
+ import { color, withCommandJournal, JsonStreamWriter, detectAdapter, handleCallHelp } from "@farmslot/recipe-cli/harness";
8
8
  import { handleUpdate, maybeNudge } from "./commands/update.js";
9
9
  import { handlePin } from "./commands/pin.js";
10
- import { handleCallHelp } from "./commands/call.js";
10
+ import { metaMaskEngine } from "./engine.js";
11
11
  import { handleHelp } from "./commands/help.js";
12
12
  import { handleTutorial } from "./commands/tutorial.js";
13
13
  import { handleSetupBase } from "./commands/setup-base.js";
14
+ import { loadSelectedAdapter } from "./adapters/plugin-surface.js";
14
15
  import { getAdapterSurface } from "./adapters/surface.js";
16
+ import { ActionManifestError } from "./manifest.js";
15
17
  import { configPath, hydrateLibraryPathEnv, readConfig } from "./review/knowledge.js";
16
18
  import {
17
19
  PUBLIC_COMMAND_CONTRACTS,
@@ -1088,6 +1090,17 @@ async function delegate(argv) {
1088
1090
  try {
1089
1091
  return await recipeMain(argv);
1090
1092
  } catch (error) {
1093
+ const own = argv.includes("--") ? argv.slice(0, argv.indexOf("--")) : argv;
1094
+ if (error instanceof ActionManifestError && own.includes("--json")) {
1095
+ console.log(JSON.stringify({
1096
+ schemaVersion: 1,
1097
+ command: argv[0],
1098
+ status: "fail",
1099
+ exitCode: 1,
1100
+ error: { code: error.code, message: error.message, userAction: error.userAction }
1101
+ }, null, 2));
1102
+ return 1;
1103
+ }
1091
1104
  console.error(error instanceof Error ? error.message : String(error));
1092
1105
  return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
1093
1106
  }
@@ -1226,11 +1239,15 @@ if (!preflightBypass.has(rawArgv[0] ?? "")) {
1226
1239
  }
1227
1240
  }
1228
1241
  await hydrateLibraryPathEnv({ target: targetFromArgv(rawArgv), config: readConfig(configPath()) });
1242
+ {
1243
+ const refused = await loadSelectedAdapter(rawArgv[0] ?? "", rawArgv.slice(1));
1244
+ if (refused !== void 0) process.exit(refused);
1245
+ }
1229
1246
  if (hasPassthroughHelp(rawArgv)) {
1230
1247
  process.exit(await delegate(rawArgv));
1231
1248
  }
1232
1249
  if (isCallActionHelp(rawArgv)) {
1233
1250
  const callHelp = REAL.find((command) => command.name === "call")?.helpText ?? "";
1234
- process.exit(await handleCallHelp(rawArgv.slice(1), callHelp));
1251
+ process.exit(await handleCallHelp(rawArgv.slice(1), callHelp, { catalog: metaMaskEngine }));
1235
1252
  }
1236
1253
  await program.parseAsync(process.argv);
@@ -15,19 +15,13 @@ const sessions = /* @__PURE__ */ new Map();
15
15
  const DEFAULT_MAX_DURATION_MS = 5 * 60 * 1e3;
16
16
  const MAX_DURATION_MS = 60 * 60 * 1e3;
17
17
  const gzipAsync = promisify(gzip);
18
- function startRunPerformanceObservation(adapter, target, artifactsDir, env, ports = {}) {
19
- const observer = getAdapterSurface(adapter).observation?.performance;
20
- if (!observer) return void 0;
18
+ function startRunPerformanceObservation(observer, { target, artifactsDir, env }) {
21
19
  const key = path.resolve(artifactsDir);
22
20
  const session = {
23
21
  observer,
24
22
  artifactsDir: key,
25
- env: {
26
- ...env,
27
- ...ports.watcherPort ? { WATCHER_PORT: ports.watcherPort } : {}
28
- },
23
+ env,
29
24
  nodeEvents: [],
30
- ports,
31
25
  target,
32
26
  windows: /* @__PURE__ */ new Map()
33
27
  };
@@ -154,7 +148,7 @@ async function performanceBackend(session) {
154
148
  if (session.backend) return session.backend;
155
149
  if (session.backendError) throw new Error(session.backendError);
156
150
  try {
157
- session.backend = await session.observer.backend(session.target, session.env, session.ports);
151
+ session.backend = await session.observer.backend(session.target, session.env);
158
152
  return session.backend;
159
153
  } catch (error) {
160
154
  session.backendError = errorMessage(error);
@@ -517,13 +511,11 @@ function unavailableCapture(session, window, reason) {
517
511
  session.nodeEvents
518
512
  );
519
513
  }
520
- async function createExtensionPerformanceObserver(target, env, ports) {
521
- return createExtensionPerformanceBackend(extensionCdpPort(ports, env), target);
514
+ async function createExtensionPerformanceObserver(target, env) {
515
+ return createExtensionPerformanceBackend(extensionCdpPort(env), target);
522
516
  }
523
- function extensionCdpPort(ports, env) {
524
- const port = Number(
525
- ports.cdpPort ?? env.CDP_PORT ?? env.RECIPE_CDP_PORT
526
- );
517
+ function extensionCdpPort(env) {
518
+ const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
527
519
  if (!Number.isInteger(port) || port <= 0) {
528
520
  throw new Error("Extension performance observation requires CDP_PORT.");
529
521
  }
@@ -1,11 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { loadRecipeLibraries, resolveRecipeDependencies, rootResolutionRef } from "@farmslot/recipe-runner";
4
- import { isSensitiveKey, CliError, isRecord, optionStrings, EXIT } from "@farmslot/recipe-cli/harness";
4
+ import { isSensitiveKey, CliError, describeManifestActions, isRecord, optionStrings, EXIT, resolveLibrarySources, validateRunRecipeStatic } from "@farmslot/recipe-cli/harness";
5
5
  import { resolveActionManifest } from "./manifest.js";
6
- import { describeManifestActions } from "./commands/manifest.js";
7
6
  import { recipeComposition } from "@farmslot/recipe-cli";
8
- import { resolveMetaMaskLibrarySources, validateRunRecipeStatic } from "./commands/run-engine.js";
7
+ import { metaMaskEngine } from "./engine.js";
9
8
  import { MAX_ADVICE_STATE_BYTES } from "./recipe-assistance-rubric.js";
10
9
  function digest(value) {
11
10
  return createHash("sha256").update(value).digest("hex");
@@ -46,7 +45,7 @@ async function buildAdviceContext(input) {
46
45
  const task = readInput(input.task);
47
46
  if (!input.recipe) throw new CliError("Advice requires a recipe. Next: use recipe-quality plan before authoring.", EXIT.usage);
48
47
  readInput(input.recipe);
49
- const validated = await validateRunRecipeStatic(input.recipe, input.adapter, input.options ?? {}, input.params);
48
+ const validated = await validateRunRecipeStatic(metaMaskEngine, input.recipe, input.adapter, input.options ?? {}, input.params);
50
49
  if (validated.usageError || !validated.schemaValid || !isRecord(validated.recipe)) {
51
50
  const detail = validated.usageError?.message ?? validated.findings.map((finding) => finding.message).join("; ");
52
51
  throw new CliError(`Recipe is not statically valid: ${detail}. Next: run mm-harness run ${input.recipe} --plan --adapter ${input.adapter}.`, EXIT.validation);
@@ -56,7 +55,7 @@ async function buildAdviceContext(input) {
56
55
  const dependencies = resolveRecipeDependencies({ rootRef: rootResolutionRef(rootDigest), root: validated.recipe, rootSource: { kind: "recipe-file", trust: "unknown", digest: rootDigest }, recipes: library.recipes });
57
56
  const composition = recipeComposition(validated.recipe, dependencies.recipes);
58
57
  const { manifest, actionSources } = await resolveActionManifest(input.adapter, void 0, validated.librarySources);
59
- const catalog = describeManifestActions(manifest, actionSources);
58
+ const catalog = describeManifestActions(metaMaskEngine, manifest, actionSources);
60
59
  const used = new Set(composition.actions);
61
60
  const actions = catalog.filter((action) => used.has(action.name)).map(({ name, description, schema, capabilities }) => ({ name, description, schema, capabilities }));
62
61
  const domains = new Set(composition.actions.map((name) => name.split(".").slice(0, -1).join(".")));
@@ -108,12 +107,12 @@ async function buildPlanningContext(input) {
108
107
  throw new CliError("Planning requires 1\u201320 non-empty acceptanceCriteria strings. Next: supply a focused JSON task.", EXIT.validation);
109
108
  }
110
109
  const criteria = task.acceptanceCriteria;
111
- const sources = await resolveMetaMaskLibrarySources(optionStrings(input.options ?? {}, "library"));
110
+ const sources = await resolveLibrarySources(metaMaskEngine, optionStrings(input.options ?? {}, "library"));
112
111
  const library = await loadRecipeLibraries(sources ?? [], { adapter: input.adapter });
113
112
  const { manifest, actionSources } = await resolveActionManifest(input.adapter, void 0, sources);
114
113
  const candidates = [
115
114
  ...[...library.recipes.values()].map((recipe) => ({ id: `recipe:${recipe.ref}`, kind: "recipe", ref: recipe.ref, description: typeof recipe.document.description === "string" ? recipe.document.description : "", parameters: recipe.document.paramsSchema, source: recipe.source, provenance: recipe.provenance, shadows: recipe.shadows })),
116
- ...describeManifestActions(manifest, actionSources).map((action) => ({ id: `action:${action.name}`, kind: "action", ref: action.name, description: action.description, parameters: action.schema, capabilities: action.capabilities, source: action.source, sourceTier: action.sourceTier, shadows: action.shadows }))
115
+ ...describeManifestActions(metaMaskEngine, manifest, actionSources).map((action) => ({ id: `action:${action.name}`, kind: "action", ref: action.name, description: action.description, parameters: action.schema, capabilities: action.capabilities, source: action.source, sourceTier: action.sourceTier, shadows: action.shadows }))
117
116
  ];
118
117
  const terms = new Set(criteria.join(" ").toLowerCase().match(/[a-z][a-z_]{3,}/gu) ?? []);
119
118
  const score = (candidate) => [...new Set(`${candidate.ref} ${candidate.description}`.toLowerCase().match(/[a-z][a-z_]{3,}/gu) ?? [])].filter((word) => terms.has(word)).length;
package/dist/runner.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
3
4
  import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
4
5
  import { getAdapterSurface } from "./adapters/surface.js";
@@ -8,6 +9,7 @@ import {
8
9
  resolveMetaMaskMobileLifecycleTarget
9
10
  } from "./app-lifecycle.js";
10
11
  import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
12
+ import { adapterPlugin } from "@farmslot/recipe-cli/harness";
11
13
  import { actionSourceIntegrity, metaMaskActionExecutionCapabilities } from "./recipe-security.js";
12
14
  import {
13
15
  createRecordingTargetProvider,
@@ -23,6 +25,7 @@ import {
23
25
  runnerDir
24
26
  } from "./paths.js";
25
27
  import {
28
+ isFixturePolicyBase,
26
29
  selectedIdentityFromWalletImport,
27
30
  trustedMutationForAction
28
31
  } from "./funding-execution-context.js";
@@ -49,7 +52,12 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
49
52
  const { createReactNativeBridgeUiTransport } = await importRecipeHarnessRuntimeReactNativeBridge();
50
53
  const { createAppLifecycleAdapters } = await importRecipeHarnessAppLifecycle();
51
54
  const actions = Object.keys(actionManifest.actions);
52
- const customActions = actions.filter((action) => !OFFICIAL_ACTIONS.has(action));
55
+ const surface = getAdapterSurface(adapter);
56
+ const coded = await surface.actions.adapters?.() ?? [];
57
+ const codedActions = new Set(coded.map((entry) => entry.action));
58
+ const customActions = actions.filter(
59
+ (action) => !OFFICIAL_ACTIONS.has(action) && !codedActions.has(action)
60
+ );
53
61
  const actionSources = options.actionSources ? new Map([...options.actionSources].map(([action, source]) => [action, { ...source }])) : void 0;
54
62
  const preparedLiveAdapters = await prepareLiveAdapterImplementations(
55
63
  adapter,
@@ -57,6 +65,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
57
65
  actionSources,
58
66
  options.trustTaskActions
59
67
  );
68
+ bindPluginDigest(actionSources, codedActions, adapterPlugin(adapter)?.digest);
60
69
  const declaredActions = new Set(actions);
61
70
  const core = createStandardCoreAdapters({ actions });
62
71
  const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status", "app.lifecycle"]);
@@ -72,7 +81,6 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
72
81
  }, preparedLiveAdapters, { hudPolicy: options.autoHud === true ? "show" : "auto" })
73
82
  });
74
83
  const existing = new Set([...core, ...ui].map((entry) => entry.action));
75
- const surface = getAdapterSurface(adapter);
76
84
  const lifecycleAdapters = bindMetaMaskMobileLifecycleDeadline(
77
85
  createAppLifecycleAdapters({
78
86
  actions,
@@ -81,11 +89,12 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
81
89
  })
82
90
  );
83
91
  const lifecycle = surface.actions.lifecycle ? await surface.actions.lifecycle(lifecycleAdapters) : lifecycleAdapters;
84
- const custom = createMetaMaskAdapters(
85
- adapter,
86
- customActions,
87
- preparedLiveAdapters
88
- ).filter(
92
+ const custom = [
93
+ ...coded,
94
+ ...createMetaMaskAdapters(adapter, customActions, preparedLiveAdapters).filter(
95
+ (entry) => !codedActions.has(entry.action)
96
+ )
97
+ ].filter(
89
98
  (entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
90
99
  );
91
100
  const autoHudDisabled = options.autoHud === false || options.autoHud === void 0 && (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false");
@@ -166,7 +175,7 @@ function withTrustedMutationContext(adapters, base, externalMutationActions) {
166
175
  throw error;
167
176
  });
168
177
  if (invalidatesIdentity) selectedIdentitySha256 = void 0;
169
- const selected = selectedIdentityFromWalletImport(entry.action, result, base);
178
+ const selected = isFixturePolicyBase(base) ? void 0 : selectedIdentityFromWalletImport(entry.action, result, base);
170
179
  if (selected) {
171
180
  if (base && base.selectedIdentitySha256 !== selected) {
172
181
  throw new Error("Same-run wallet identity changed after its trusted import proof.");
@@ -196,6 +205,17 @@ function deduplicateLibraryResolutionLogs(logger, suppress = false) {
196
205
  }
197
206
  };
198
207
  }
208
+ function bindPluginDigest(sources, actions, pluginDigest) {
209
+ if (!sources || !pluginDigest) return;
210
+ const bind = (digest) => `sha256:${createHash("sha256").update(`${digest}\0${pluginDigest}`).digest("hex")}`;
211
+ for (const action of actions) {
212
+ const source = sources.get(action);
213
+ if (!source) continue;
214
+ const { digest, resolveDigest } = source;
215
+ source.digest = bind(digest ?? "");
216
+ source.resolveDigest = resolveDigest ? async () => bind(await resolveDigest()) : void 0;
217
+ }
218
+ }
199
219
  async function prepareLiveAdapterImplementations(platform, actions, sources, trustTaskActions = false) {
200
220
  const prepared = /* @__PURE__ */ new Map();
201
221
  if (!sources) return prepared;
@@ -79,8 +79,10 @@ context, prepare) and `readiness.ts` (doctor and status). Do not add
79
79
  command-local platform ladders for surface-owned behavior:
80
80
  - `launch` dispatches through `AdapterSurface.launch` (the device platforms
81
81
  share `src/commands/launch/device.ts` through a `DeviceLaunchProfile`);
82
- - `run`/`call` read `run`, `recording`, `diagnostics`, `observation` and
83
- `sourceFingerprint`;
82
+ - `run`/`call` come from `@farmslot/recipe-cli/harness` and read `run`,
83
+ `recording`, `diagnostics`, `observation` and `sourceFingerprint`;
84
+ mm-harness hands them its engine (`src/engine.ts`) and its device targeting
85
+ and plan steps (`src/cli.ts`);
84
86
  - the runner reads `actions` (manifest, bundled semantic actions, live adapter
85
87
  scripts, `cdp.target`, the `ui.*` transport, adapter wrappers, input checks);
86
88
  - `install`/`verify`/`cleanup` read `harness`, `agentic-runtime.json` reads
package/docs/RECIPES.md CHANGED
@@ -245,9 +245,37 @@ Use the snapshot recipe's `history` object to bound existing record dates and
245
245
  `capture_path` to separate evidence when composing calls. Market sampling does
246
246
  not repeatedly fetch history.
247
247
 
248
- The two live recipes require explicit `confirm_live=true`. The trusted runner
249
- must also verify the exact consented funding request, preflight, release proof,
250
- and contained balance proof. Market placement declares the exact provider and
248
+ Perps orders follow one rule on Mobile, Extension and Core
249
+ (`library/actions/shared/perps/order-policy.mjs`), checked before dispatch:
250
+
251
+ - Testnet: allowed with no funding proof.
252
+ - Mainnet: only from a synced fixture account (`wallet-fixture.json`), as a 1x
253
+ isolated market order with `close_immediately: true`, and the node must
254
+ declare `network: mainnet`. The $15 limit bounds the size the controller would
255
+ submit: (amount + one size step × price) × (1 + max slippage). Anything else
256
+ is refused with `PERPS_MAINNET_ORDER_REFUSED`, naming each broken limit, and
257
+ no order is sent.
258
+ - `close_immediately: true` needs a market order from a flat market: no
259
+ position and no resting order in it. Positions are netted per market, so the
260
+ close takes the market's whole position; don't share a fixture account's
261
+ market with a concurrent run. The action closes the opened position itself
262
+ and proves the market flat. On mainnet it also checks the position opened
263
+ isolated 1x and within $15. A placement that fails after dispatch has its
264
+ position closed too.
265
+ - The account and network the rule allowed are rechecked around the flat-market
266
+ reads, before dispatch and before any close; if either changed, nothing is
267
+ sent or closed.
268
+ - Core still needs `CORE_PERPS_ALLOW_MAINNET_WRITES=1` for any mainnet write.
269
+
270
+ On the Extension, a run without funding files gets that rule through the
271
+ fixture-policy lane: the runner binds the run and a run-owned execution ledger
272
+ (`<artifacts>/mutation-ledger`), and each mutation completes the context from
273
+ the live app, so receipts and resume work as in a funded run. The funding files
274
+ below remain the stricter release lane.
275
+
276
+ The two live recipes require explicit `confirm_live=true`. A funded run's
277
+ trusted runner must also verify the exact consented funding request, preflight,
278
+ release proof, and contained balance proof. Market placement declares the exact provider and
251
279
  network; background placement and cancellation refuse a mismatch without
252
280
  toggling it. Immediately before publication and dispatch, the action also reads
253
281
  the current visible Perps Home balance and requires the runner-trusted minimum.
@@ -92,9 +92,11 @@ These are real farm bugs. The goldens freeze the current rejections, so fixing e
92
92
 
93
93
  ## Terminal adapter
94
94
 
95
- `goldens-terminal` freezes the `va-mmcx-terminal-farm` hooks with their exact argv (`recipe_action_manifest`, `recipe_doctor`, `recipe_harness_{install,verify,cleanup}`, `recipe_run`, the browser `boot`/`shutdown` resources and `teardown`), against a fresh fixture slot: no slot dev server, no Extension dist, and the farm's wallet fixture outside the slot, so `install` copies it in. `launch` is frozen on its cold path: it refuses at the missing dist before any browser starts. The slot's CDP port is the one port no port holder reserves: a farm slot with no browser has it free, and the Terminal's port checks probe it. `goldens-discovery` covers `terminal` in its per-adapter loop, so `terminal/actions-raw` and `discovery/actions-raw-terminal` freeze the same manifest and change together.
95
+ `goldens-terminal` freezes the `va-mmcx-terminal-farm` hooks with their exact argv (`recipe_action_manifest`, `recipe_doctor`, `recipe_harness_{install,verify,cleanup}`, `recipe_run`, the browser `boot`/`shutdown` resources and `teardown`), against a fresh fixture slot: no slot dev server, no Extension dist, and the farm's wallet fixture outside the slot, so `install` copies it in. `launch` is frozen on its cold path: it refuses at the missing dist before any browser starts. The slot's CDP port is the one port no port holder reserves: a farm slot with no browser has it free, and the Terminal's port checks probe it.
96
96
 
97
- Outside this boundary: a successful `launch` and the `temp/recipe/runtime/terminal/` layout (`browser.pid`), recipes and actions from the farm's `RECIPE_LIBRARY_PATH` libraries, `run terminal.perps.<name> --plan`, and the recipe-terminal `console-guard.mjs` import paths.
97
+ `terminal` is the plugin the `experimental-metamask-recipe-terminal` library declares (`adapters.terminal`, extends the built-in `web-dapp`), so the family loads it: set `GOLDENS_TERMINAL_LIBRARY` to a checkout of that library, which the family copies into its sandbox and puts on `RECIPE_LIBRARY_PATH`. CI has no access to the private library and skips the family (`CI` set); local runs need the variable, and without it outside CI the family fails. A release gate must set it too (proposed for the release checklist). `goldens-discovery` covers the built-in `web-dapp` in its per-adapter loop.
98
+
99
+ Outside this boundary: a successful `launch` and the `temp/recipe/runtime/terminal/` layout (`browser.pid`), the library's recipes, `run terminal.perps.<name> --plan`, and the recipe-terminal `console-guard.mjs` import paths.
98
100
 
99
101
  ## Running the goldens
100
102
 
@@ -142,7 +144,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
142
144
  2. UUIDs become `<UUID>`. Git object ids (40 hex, or a short hex value under a `*head`/`*gitRef`/`*commit` key) become `<GIT_REF>`. ISO-8601 timestamps become `<TIMESTAMP>`, and compact stamps in names `<STAMP>`. Path-derived slot ids `local-<adapter>-<8 hex>` become `local-<adapter>-<HASH>`.
143
145
  3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
144
146
  4. Numbers under volatile keys: `*pid` becomes `<PID>`, `inode` `<INODE>`, `*port` `<PORT>`, `*At`/`*Time`/`*timestamp`/`mtime*` `<TIMESTAMP>`, and `*Ms`/`duration*`/`elapsed*` `<DURATION>`. Values inside `schema` and `examples` subtrees (static manifest content) are never rewritten.
145
- 5. `invocationDigest` becomes `<DIGEST>`: it hashes `recipe-invocation.json`, which carries sandbox paths and timestamps and is frozen itself in normalised form. `runner.sourceFingerprint` becomes `<FINGERPRINT>`: it hashes the harness source tree, so any harness edit changes it. A `status` holding `git status --porcelain` output (provenance `runner.status` and `libraries[].status`, empty when clean) becomes `<GIT_STATUS>`. Content-only digests (recipe and dependency digests) stay frozen.
147
+ 5. `invocationDigest` becomes `<DIGEST>`: it hashes `recipe-invocation.json`, which carries sandbox paths and timestamps and is frozen itself in normalised form. `runner.sourceFingerprint` becomes `<FINGERPRINT>`: it hashes the harness source tree, so any harness edit changes it. A `status` holding `git status --porcelain` output (provenance `runner.status` and `libraries[].status`, empty when clean) becomes `<GIT_STATUS>`. A loaded adapter plugin's code digest in doctor's `adapter-plugin:<id>` message becomes `sha256:<PLUGIN_DIGEST>`: it hashes the recipe library's plugin files, so any library edit changes it. Content-only digests (recipe and dependency digests) stay frozen.
146
148
  6. Durations in text (`0.3s`, `123ms`) become `<DURATION>`.
147
149
 
148
150
  In tree listings, files whose names differ only by a volatile token collapse into one row: a count and the union of their key shapes, or the sorted list of their values in a value tree.