@deeeed/metamask-harness 0.81.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 (71) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/adapters/manifest.json +51 -51
  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 +23 -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 +15 -25
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +11 -15
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +10 -11
  16. package/dist/adapters/extension/run.js +5 -28
  17. package/dist/adapters/plugin-surface.js +98 -0
  18. package/dist/adapters/surface.js +1 -1
  19. package/dist/adapters/{terminal → web-dapp}/actions.js +7 -7
  20. package/dist/adapters/web-dapp/console-capture.js +63 -0
  21. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  22. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -22
  23. package/dist/adapters/web-dapp/overlay.js +21 -0
  24. package/dist/adapters/web-dapp/readiness.js +10 -0
  25. package/dist/adapters/{terminal → web-dapp}/run.js +15 -15
  26. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  27. package/dist/adapters/web-dapp/surface.js +149 -0
  28. package/dist/cli.js +3 -0
  29. package/dist/command-contract.js +29 -7
  30. package/dist/commands/check.js +2 -2
  31. package/dist/commands/doctor.js +19 -2
  32. package/dist/commands/farmslot-ready.js +1 -0
  33. package/dist/commands/help.js +1 -1
  34. package/dist/commands/launch/device.js +5 -5
  35. package/dist/commands/self-test.js +9 -5
  36. package/dist/engine.js +6 -16
  37. package/dist/manifest.js +48 -24
  38. package/dist/mm-harness-cli.js +17 -0
  39. package/dist/performance-observation.js +7 -13
  40. package/dist/runner.js +26 -7
  41. package/docs/cli-contract.md +5 -3
  42. package/library/actions/shared/console-collector.mjs +1 -1
  43. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  44. package/library/actions/{terminal → web-dapp}/app/launch.mjs +7 -7
  45. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +5 -5
  46. package/library/actions/{terminal → web-dapp}/platform/page.mjs +11 -11
  47. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  48. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  49. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  50. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +4 -4
  51. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  52. package/library/actions/{terminal → web-dapp}/wallet/read_signatures.mjs +6 -6
  53. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  54. package/package.json +5 -5
  55. package/adapters/terminal/lib/mainnet-guard.mjs +0 -186
  56. package/adapters/terminal/policy.mjs +0 -35
  57. package/dist/adapters/terminal/console-capture.js +0 -59
  58. package/dist/adapters/terminal/doctor-checks.js +0 -24
  59. package/dist/adapters/terminal/overlay.js +0 -25
  60. package/dist/adapters/terminal/readiness.js +0 -11
  61. package/dist/adapters/terminal/surface.js +0 -135
  62. package/library/actions/terminal/perps/_venue.mjs +0 -242
  63. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  64. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  65. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  66. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  67. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  68. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  69. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  70. package/library/actions/terminal/ui/navigate.mjs +0 -31
  71. package/library/actions/terminal/wallet/_signature-log.mjs +0 -56
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
  };
@@ -11,7 +11,9 @@ 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,6 +1239,10 @@ 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
  }
@@ -15,17 +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(observer, { target, artifactsDir, env, ports }) {
18
+ function startRunPerformanceObservation(observer, { target, artifactsDir, env }) {
19
19
  const key = path.resolve(artifactsDir);
20
20
  const session = {
21
21
  observer,
22
22
  artifactsDir: key,
23
- env: {
24
- ...env,
25
- ...ports.watcherPort ? { WATCHER_PORT: ports.watcherPort } : {}
26
- },
23
+ env,
27
24
  nodeEvents: [],
28
- ports,
29
25
  target,
30
26
  windows: /* @__PURE__ */ new Map()
31
27
  };
@@ -152,7 +148,7 @@ async function performanceBackend(session) {
152
148
  if (session.backend) return session.backend;
153
149
  if (session.backendError) throw new Error(session.backendError);
154
150
  try {
155
- session.backend = await session.observer.backend(session.target, session.env, session.ports);
151
+ session.backend = await session.observer.backend(session.target, session.env);
156
152
  return session.backend;
157
153
  } catch (error) {
158
154
  session.backendError = errorMessage(error);
@@ -515,13 +511,11 @@ function unavailableCapture(session, window, reason) {
515
511
  session.nodeEvents
516
512
  );
517
513
  }
518
- async function createExtensionPerformanceObserver(target, env, ports) {
519
- return createExtensionPerformanceBackend(extensionCdpPort(ports, env), target);
514
+ async function createExtensionPerformanceObserver(target, env) {
515
+ return createExtensionPerformanceBackend(extensionCdpPort(env), target);
520
516
  }
521
- function extensionCdpPort(ports, env) {
522
- const port = Number(
523
- ports.cdpPort ?? env.CDP_PORT ?? env.RECIPE_CDP_PORT
524
- );
517
+ function extensionCdpPort(env) {
518
+ const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
525
519
  if (!Number.isInteger(port) || port <= 0) {
526
520
  throw new Error("Extension performance observation requires CDP_PORT.");
527
521
  }
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,
@@ -50,7 +52,12 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
50
52
  const { createReactNativeBridgeUiTransport } = await importRecipeHarnessRuntimeReactNativeBridge();
51
53
  const { createAppLifecycleAdapters } = await importRecipeHarnessAppLifecycle();
52
54
  const actions = Object.keys(actionManifest.actions);
53
- 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
+ );
54
61
  const actionSources = options.actionSources ? new Map([...options.actionSources].map(([action, source]) => [action, { ...source }])) : void 0;
55
62
  const preparedLiveAdapters = await prepareLiveAdapterImplementations(
56
63
  adapter,
@@ -58,6 +65,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
58
65
  actionSources,
59
66
  options.trustTaskActions
60
67
  );
68
+ bindPluginDigest(actionSources, codedActions, adapterPlugin(adapter)?.digest);
61
69
  const declaredActions = new Set(actions);
62
70
  const core = createStandardCoreAdapters({ actions });
63
71
  const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status", "app.lifecycle"]);
@@ -73,7 +81,6 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
73
81
  }, preparedLiveAdapters, { hudPolicy: options.autoHud === true ? "show" : "auto" })
74
82
  });
75
83
  const existing = new Set([...core, ...ui].map((entry) => entry.action));
76
- const surface = getAdapterSurface(adapter);
77
84
  const lifecycleAdapters = bindMetaMaskMobileLifecycleDeadline(
78
85
  createAppLifecycleAdapters({
79
86
  actions,
@@ -82,11 +89,12 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
82
89
  })
83
90
  );
84
91
  const lifecycle = surface.actions.lifecycle ? await surface.actions.lifecycle(lifecycleAdapters) : lifecycleAdapters;
85
- const custom = createMetaMaskAdapters(
86
- adapter,
87
- customActions,
88
- preparedLiveAdapters
89
- ).filter(
92
+ const custom = [
93
+ ...coded,
94
+ ...createMetaMaskAdapters(adapter, customActions, preparedLiveAdapters).filter(
95
+ (entry) => !codedActions.has(entry.action)
96
+ )
97
+ ].filter(
90
98
  (entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
91
99
  );
92
100
  const autoHudDisabled = options.autoHud === false || options.autoHud === void 0 && (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false");
@@ -197,6 +205,17 @@ function deduplicateLibraryResolutionLogs(logger, suppress = false) {
197
205
  }
198
206
  };
199
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
+ }
200
219
  async function prepareLiveAdapterImplementations(platform, actions, sources, trustTaskActions = false) {
201
220
  const prepared = /* @__PURE__ */ new Map();
202
221
  if (!sources) return prepared;
@@ -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.
@@ -15,7 +15,7 @@ export const COLLECTOR_SCRIPT_NAME = 'console-tail.mjs';
15
15
  export const CAPTURE_CONTROL_MARK = '[mm-harness-capture-control]';
16
16
 
17
17
  // Same derivation as ownerMarker('console-collector', …) in
18
- // adapters/terminal/lib/processes.mjs, so the terminal stop path finds it.
18
+ // adapters/web-dapp/lib/processes.mjs, so the web-dapp stop path finds it.
19
19
  export function collectorOwnerMarker(runtimeDir) {
20
20
  const id = createHash('sha256').update(path.resolve(runtimeDir)).digest('hex').slice(0, 32);
21
21
  return `--mm-harness-owner=console-collector-${id}`;
@@ -1,4 +1,4 @@
1
- // Fail the recipe when the terminal page logged an error or uncaught exception
1
+ // Fail the recipe when the app page logged an error or uncaught exception
2
2
  // that the recipe libraries' console allowlist does not mark as known
3
3
  // third-party noise. Reads the slot console capture (app-console.log, and
4
4
  // extension-console.log when asked) from where the current slot browser's
@@ -20,7 +20,7 @@ import {
20
20
  loadConsoleAllowlist,
21
21
  } from '../../shared/console-findings.mjs';
22
22
  import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
23
- import { isDirectRun, readBrowserState, runAdapter, terminalRuntimeDir } from '../platform/runtime.mjs';
23
+ import { isDirectRun, readBrowserState, runAdapter, webDappPolicy, webDappRuntimeDir } from '../platform/runtime.mjs';
24
24
 
25
25
  const LOGS = Object.freeze({ page: 'app-console.log', extension: 'extension-console.log' });
26
26
 
@@ -55,7 +55,7 @@ export async function assertNoConsoleErrors(input, env = process.env, deps = {})
55
55
  const projectRoot = input.context?.projectRoot;
56
56
  const sources = Array.isArray(node.sources) && node.sources.length ? node.sources.map(String) : ['page', 'browser'];
57
57
  const levels = new Set(Array.isArray(node.levels) && node.levels.length ? node.levels.map(String) : ['error', 'exception']);
58
- const runtime = terminalRuntimeDir(projectRoot);
58
+ const runtime = webDappRuntimeDir(projectRoot);
59
59
  const state = await readBrowserState(projectRoot);
60
60
  if (!state?.cdpPort || !state.appOrigin) throw unavailable('no slot browser state (browser.json)');
61
61
  const capture = (deps.collectorStatus ?? collectorStatus)({
@@ -107,7 +107,7 @@ export async function assertNoConsoleErrors(input, env = process.env, deps = {})
107
107
  // The recorded launch offset was past the end of a replaced capture
108
108
  // file; that file was read from its start.
109
109
  offsetResets,
110
- proofPath: 'terminal-console-capture',
110
+ proofPath: `${webDappPolicy().adapterId}-console-capture`,
111
111
  };
112
112
  if (failing.size > 0) {
113
113
  const list = [...failing.values()].map((item) => `${item.level} [${item.source}] ${item.example} ×${item.count}`).join('\n ');
@@ -1,15 +1,15 @@
1
- // Explicit lifecycle step: start (or reuse) the terminal slot browser with the
1
+ // Explicit lifecycle step: start (or reuse) the web-dapp slot browser with the
2
2
  // requested signer (MetaMask extension or injected strict wallet) and fixture
3
3
  // account, and the wallet host that records wallet requests. A fresh_profile
4
4
  // start forgets the app's stored agent key and the extension's dapp permission.
5
5
 
6
- import { appPort, cdpPort, isDirectRun, resolveSigner, runAdapter } from '../platform/runtime.mjs';
7
- import { launchTerminalBrowser } from '../../../../adapters/terminal/launch.mjs';
8
- import { policy } from '../../../../adapters/terminal/policy.mjs';
6
+ import { appPort, cdpPort, isDirectRun, resolveSigner, runAdapter, webDappPolicy } from '../platform/runtime.mjs';
7
+ import { launchWebDappBrowser } from '../../../../adapters/web-dapp/launch.mjs';
9
8
 
10
9
  export async function launch(input) {
11
10
  const node = input.node ?? {};
12
- const state = await launchTerminalBrowser({
11
+ const policy = webDappPolicy();
12
+ const state = await launchWebDappBrowser({
13
13
  target: input.context?.projectRoot,
14
14
  'cdp-port': cdpPort(input),
15
15
  'app-port': appPort(input),
@@ -24,7 +24,7 @@ export async function launch(input) {
24
24
  network: node.network,
25
25
  'mainnet-confirmation': node.mainnet_confirmation,
26
26
  'start-chain': node.start_chain ?? policy.startChain,
27
- }, process.env, { policy });
27
+ }, process.env);
28
28
  return {
29
29
  action: input.action,
30
30
  status: 'ready',
@@ -39,7 +39,7 @@ export async function launch(input) {
39
39
  slowMoMs: state.slowMoMs ?? 0,
40
40
  extension: state.extension ? { id: state.extension.id, version: state.extension.version } : null,
41
41
  freshProfile: state.freshProfile,
42
- proofPath: 'terminal-browser-launch',
42
+ proofPath: `${policy.adapterId}-browser-launch`,
43
43
  };
44
44
  }
45
45
 
@@ -1,12 +1,12 @@
1
- // app.hud for the terminal: fold the runner's progress update into
1
+ // app.hud for web-dapp: fold the runner's progress update into
2
2
  // <runtime>/hud.json and draw it on the app tab now. The wallet host redraws
3
3
  // the same state after every navigation. The HUD is advisory: a missing
4
4
  // browser or a busy tab skips the update instead of failing the recipe.
5
5
 
6
6
  import { isAppUrl } from '@farmslot/adapter-web/origin';
7
7
 
8
- import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from '../../../../adapters/terminal/lib/hud.mjs';
9
- import { appOrigin, cdpPort, readBrowserState, terminalRuntimeDir } from './runtime.mjs';
8
+ import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from '../../../../adapters/web-dapp/lib/hud.mjs';
9
+ import { appOrigin, cdpPort, readBrowserState, webDappRuntimeDir } from './runtime.mjs';
10
10
  import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
11
11
 
12
12
  // --hud show draws in any mode; --hud auto (the default) only for a visible
@@ -47,14 +47,14 @@ async function drawOnAppTabs(input, expression, timeoutMs = 3000) {
47
47
  return results;
48
48
  }
49
49
 
50
- export async function showTerminalHud(input, { hudPolicy = 'auto' } = {}) {
50
+ export async function showAppHud(input, { hudPolicy = 'auto' } = {}) {
51
51
  const node = input.node ?? {};
52
52
  const projectRoot = input.context?.projectRoot;
53
53
  const nodeId = input.context?.nodeId ?? '';
54
54
  const browserState = projectRoot ? await readBrowserState(projectRoot) : null;
55
55
  if (!browserState) return { hud: false, skipped: true, reason: 'no slot browser yet' };
56
56
  if (!hudVisible(hudPolicy, browserState)) return { hud: false, skipped: true, reason: 'headless browser (--hud show draws anyway)' };
57
- const runtimeDir = terminalRuntimeDir(projectRoot);
57
+ const runtimeDir = webDappRuntimeDir(projectRoot);
58
58
  let state = null;
59
59
  if (node.clear === true) {
60
60
  clearHudState(runtimeDir);
@@ -1,4 +1,4 @@
1
- // Select the CDP page a terminal UI action drives: the app tab (default) or the
1
+ // Select the CDP page a web-dapp UI action drives: the app tab (default) or the
2
2
  // MetaMask wallet surface (side panel / notification / popup) that shows the
3
3
  // pending dapp request. Pages are never brought to front unless
4
4
  // FARMSLOT_FOCUS_BROWSER=1, as in the Extension adapter: Page.bringToFront
@@ -8,9 +8,9 @@
8
8
 
9
9
  import { isAppUrl } from '@farmslot/adapter-web/origin';
10
10
 
11
- import { resolveSlowMo } from '../../../../adapters/terminal/lib/display.mjs';
12
- import { HUD_ELEMENT } from '../../../../adapters/terminal/lib/hud.mjs';
13
- import { cdpListenerPids } from '../../../../adapters/terminal/lib/processes.mjs';
11
+ import { resolveSlowMo } from '../../../../adapters/web-dapp/lib/display.mjs';
12
+ import { HUD_ELEMENT } from '../../../../adapters/web-dapp/lib/hud.mjs';
13
+ import { cdpListenerPids } from '../../../../adapters/web-dapp/lib/processes.mjs';
14
14
  import { appOrigin, cdpPort, readBrowserState } from './runtime.mjs';
15
15
  import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
16
16
 
@@ -41,7 +41,7 @@ export function pickAppTarget(targets, origin) {
41
41
  return pages.find((target) => isAppUrl(target.url, origin)) ?? pages.find((target) => target.url === 'about:blank') ?? null;
42
42
  }
43
43
 
44
- export async function selectTerminalTarget(input) {
44
+ export async function selectAppTarget(input) {
45
45
  const node = input.node ?? {};
46
46
  const port = cdpPort(input);
47
47
  const surface = resolveSurface(node);
@@ -75,7 +75,7 @@ export async function selectTerminalTarget(input) {
75
75
  // the DOM instead.
76
76
  class WalletSurfacePage extends CdpWebPage {
77
77
  evaluate(expression) {
78
- return this.evaluateInIsolatedWorld(expression, 'mm-terminal-wallet-surface');
78
+ return this.evaluateInIsolatedWorld(expression, 'web-dapp-wallet-surface');
79
79
  }
80
80
  }
81
81
 
@@ -89,7 +89,7 @@ export function hudVisibilityExpression(visible) {
89
89
  return `(() => { const host = document.documentElement.querySelector(':scope > ${HUD_ELEMENT}'); if (!host) return false; host.style.visibility = ${JSON.stringify(visible ? '' : 'hidden')}; return true; })()`;
90
90
  }
91
91
 
92
- class TerminalAppPage extends CdpWebPage {
92
+ class AppPage extends CdpWebPage {
93
93
  async screenshot(context, relPath, metadata = {}) {
94
94
  const hidden = await this.evaluate(hudVisibilityExpression(false)).catch(() => false);
95
95
  try {
@@ -144,7 +144,7 @@ class TerminalAppPage extends CdpWebPage {
144
144
  }
145
145
  const top = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
146
146
  return { error: 'Target is fully covered by ' + (top ? top.tagName.toLowerCase() : 'nothing') + ': ' + ${JSON.stringify(selector)} };
147
- })()`, 'mm-terminal-click');
147
+ })()`, 'web-dapp-click');
148
148
  }
149
149
 
150
150
  async setInput(selector, value) {
@@ -217,14 +217,14 @@ export function assertAppListenersUnchanged(state, current) {
217
217
 
218
218
  const PAGE_DEPS = Object.freeze({
219
219
  listeners: (port) => cdpListenerPids(port),
220
- select: (input) => selectTerminalTarget(input),
220
+ select: (input) => selectAppTarget(input),
221
221
  connect: (url) => CdpSession.connect(url),
222
222
  browserState: (projectRoot) => readBrowserState(projectRoot),
223
223
  sleep: (ms) => sleep(ms),
224
224
  env: process.env,
225
225
  });
226
226
 
227
- export async function withTerminalPage(input, callback, deps = PAGE_DEPS) {
227
+ export async function withAppPage(input, callback, deps = PAGE_DEPS) {
228
228
  const state = input.context?.projectRoot ? await deps.browserState(input.context.projectRoot) : null;
229
229
  const delay = slowMoFor(state, deps.env);
230
230
  if (delay > 0) await deps.sleep(delay);
@@ -237,7 +237,7 @@ export async function withTerminalPage(input, callback, deps = PAGE_DEPS) {
237
237
  await session.call('Page.enable');
238
238
  await session.call('Runtime.enable');
239
239
  if (focusBrowserRequested(deps.env)) await session.call('Page.bringToFront');
240
- const page = surface === 'wallet' ? new WalletSurfacePage(session) : new TerminalAppPage(session);
240
+ const page = surface === 'wallet' ? new WalletSurfacePage(session) : new AppPage(session);
241
241
  const result = await callback(page);
242
242
  return result && typeof result === 'object' && !Array.isArray(result)
243
243
  ? { ...result, surface, cdpPort: port, targetUrl: target.url.replace(/0x[0-9a-fA-F]{40}/gu, '<address>') }
@@ -1,6 +1,6 @@
1
- // Resolve the Web Terminal slot runtime: ports, runtime paths, browser state and
2
- // wallet-fixture accounts. Key material is only ever returned as a viem account
3
- // object; callers must never log or persist it.
1
+ // Resolve the web-dapp slot runtime: the venue policy, ports, runtime paths,
2
+ // browser state and wallet-fixture accounts. Key material is only ever
3
+ // returned as a viem account object; callers must never log or persist it.
4
4
 
5
5
  import { realpathSync } from 'node:fs';
6
6
  import { readFile, writeFile } from 'node:fs/promises';
@@ -11,6 +11,9 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
11
11
 
12
12
  import { recipeRuntimeDir } from '@farmslot/recipe-cli/harness';
13
13
  import { walletFixturePath } from '../../harness-exports.mjs';
14
+ import { POLICY_ENV, webDappAdapterId, webDappPolicy } from '../../../../adapters/web-dapp/lib/policy.mjs';
15
+
16
+ export { POLICY_ENV, webDappAdapterId, webDappPolicy };
14
17
 
15
18
  export const SIGNER_MODES = Object.freeze(['extension', 'injected']);
16
19
  export const DEFAULT_ACCOUNT = 'dev1';
@@ -26,12 +29,13 @@ export function canonicalPath(target) {
26
29
  }
27
30
  }
28
31
 
29
- export function terminalRuntimeDir(projectRoot) {
30
- return path.join(canonicalPath(projectRoot), recipeRuntimeDir(), 'terminal');
32
+ // <checkout>/<runtime>/<adapter id>, e.g. temp/recipe/runtime/terminal.
33
+ export function webDappRuntimeDir(projectRoot) {
34
+ return path.join(canonicalPath(projectRoot), recipeRuntimeDir(), webDappPolicy().adapterId);
31
35
  }
32
36
 
33
- export function terminalRuntimePath(projectRoot, ...segments) {
34
- return path.join(terminalRuntimeDir(projectRoot), ...segments);
37
+ export function webDappRuntimePath(projectRoot, ...segments) {
38
+ return path.join(webDappRuntimeDir(projectRoot), ...segments);
35
39
  }
36
40
 
37
41
  function portFrom(raw, label) {
@@ -53,7 +57,7 @@ export function appPort(input = {}, env = process.env) {
53
57
  portFrom(env.WATCHER_PORT, 'WATCHER_PORT');
54
58
  if (!port) {
55
59
  throw new Error(
56
- 'terminal adapter requires the slot dev-server port.\n' +
60
+ `${webDappPolicy().adapterId} adapter requires the slot dev-server port.\n` +
57
61
  'Next: pass --watcher-port <port> (Farmslot: {{port}}) or set TERMINAL_APP_PORT.',
58
62
  );
59
63
  }
@@ -71,7 +75,7 @@ export function cdpPort(input = {}, env = process.env) {
71
75
  portFrom(env.CDP_PORT, 'CDP_PORT');
72
76
  if (!port) {
73
77
  throw new Error(
74
- 'terminal adapter requires a per-slot browser CDP port.\n' +
78
+ `${webDappPolicy().adapterId} adapter requires a per-slot browser CDP port.\n` +
75
79
  'Next: pass --cdp-port <port> (Farmslot: {{cdp_port}}).',
76
80
  );
77
81
  }
@@ -92,7 +96,7 @@ export async function writePrivateJson(file, value) {
92
96
  }
93
97
 
94
98
  export async function readBrowserState(projectRoot) {
95
- return readJsonFile(terminalRuntimePath(projectRoot, 'browser.json'));
99
+ return readJsonFile(webDappRuntimePath(projectRoot, 'browser.json'));
96
100
  }
97
101
 
98
102
  export function resolveSigner(raw) {
@@ -123,7 +127,7 @@ export async function loadWalletFixture(projectRoot, env = process.env) {
123
127
  if (!fixture) {
124
128
  throw new Error(
125
129
  `wallet fixture not found at ${file}.\n` +
126
- 'Next: mm-harness install --adapter terminal --target <checkout> with RECIPE_WALLET_FIXTURE set, or copy the fixture there (0600).',
130
+ `Next: mm-harness install --adapter ${webDappPolicy().adapterId} --target <checkout> with RECIPE_WALLET_FIXTURE set, or copy the fixture there (0600).`,
127
131
  );
128
132
  }
129
133
  if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
@@ -195,7 +199,7 @@ export async function runAdapter(callback) {
195
199
  const output = await callback(input);
196
200
  await writeFile(input.outputPath, `${JSON.stringify(output, null, 2)}\n`);
197
201
  } catch (error) {
198
- process.stderr.write(`[terminal] ${input.action} failed: ${error?.stack ?? error}\n`);
202
+ process.stderr.write(`[${webDappAdapterId()}] ${input.action} failed: ${error?.stack ?? error}\n`);
199
203
  process.exitCode = 1;
200
204
  }
201
205
  }