@deeeed/metamask-harness 0.82.1 → 0.84.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 (73) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +8 -1
  3. package/adapters/core/cleanup.sh +5 -29
  4. package/adapters/core/controller-import-probe.mjs +26 -0
  5. package/adapters/manifest.json +22 -102
  6. package/adapters/shared/session-lock.mjs +43 -0
  7. package/adapters/web-dapp/metamask-signer.mjs +212 -0
  8. package/dist/adapters/core/actions.js +0 -3
  9. package/dist/adapters/core/overlay.js +1 -5
  10. package/dist/adapters/core/readiness.js +133 -2
  11. package/dist/adapters/core/surface.js +53 -41
  12. package/dist/adapters/extension/extension-id.js +1 -0
  13. package/dist/adapters/extension/fixtures.js +60 -1
  14. package/dist/adapters/extension/network-observer.js +19 -206
  15. package/dist/adapters/extension/run.js +5 -3
  16. package/dist/adapters/extension/runtime.js +112 -20
  17. package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
  18. package/dist/adapters/mobile/devices.js +5 -1
  19. package/dist/adapters/mobile/performance-observer.js +9 -6
  20. package/dist/adapters/mobile/run.js +1 -1
  21. package/dist/adapters/mobile/surface.js +1 -2
  22. package/dist/adapters/performance/trace-kinds.js +36 -0
  23. package/dist/adapters/plugin-surface.js +34 -34
  24. package/dist/adapters/session-lock.js +23 -0
  25. package/dist/adapters/web-dapp/console-capture.js +2 -20
  26. package/dist/adapters/web-dapp/run.js +31 -25
  27. package/dist/adapters/web-dapp/surface.js +32 -108
  28. package/dist/command-contract.js +1363 -630
  29. package/dist/commands/core-readiness.js +1 -9
  30. package/dist/commands/doctor.js +1 -1
  31. package/dist/commands/launch/device.js +1 -1
  32. package/dist/doctor.js +5 -2
  33. package/dist/mm-harness-cli.js +228 -1210
  34. package/dist/performance-observation.js +28 -5
  35. package/docs/CONTRIBUTING.md +6 -4
  36. package/docs/cli-contract.md +1 -1
  37. package/docs/feature-guard.md +1 -1
  38. package/library/actions/core/perps/_controller.mjs +10 -0
  39. package/library/actions/extension/wallet/import.mjs +21 -4
  40. package/library/actions/extension/wallet/visible-session.mjs +32 -12
  41. package/library/actions/shared/console-collector.mjs +1 -1
  42. package/library/actions/web-dapp/app/launch.mjs +1 -1
  43. package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
  44. package/library/actions/web-dapp/platform/hud.mjs +1 -1
  45. package/library/actions/web-dapp/platform/page.mjs +2 -3
  46. package/library/actions/web-dapp/platform/runtime.mjs +29 -178
  47. package/package.json +7 -6
  48. package/scripts/completions.sh +1 -1
  49. package/adapters/web-dapp/cleanup.mjs +0 -69
  50. package/adapters/web-dapp/inject.mjs +0 -75
  51. package/adapters/web-dapp/launch.mjs +0 -631
  52. package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
  53. package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
  54. package/adapters/web-dapp/lib/display.mjs +0 -115
  55. package/adapters/web-dapp/lib/hud.mjs +0 -148
  56. package/adapters/web-dapp/lib/policy.mjs +0 -79
  57. package/adapters/web-dapp/lib/processes.mjs +0 -187
  58. package/adapters/web-dapp/lib/readiness.mjs +0 -272
  59. package/adapters/web-dapp/stop.mjs +0 -38
  60. package/adapters/web-dapp/verify.mjs +0 -51
  61. package/adapters/web-dapp/wallet-host.mjs +0 -520
  62. package/dist/adapters/core/live-script-env.js +0 -36
  63. package/dist/adapters/extension/browser-cdp.js +0 -174
  64. package/dist/adapters/extension/performance-observer.js +0 -75
  65. package/dist/adapters/performance/cdp-trace.js +0 -406
  66. package/dist/adapters/performance/js-task-metrics.js +0 -35
  67. package/dist/adapters/web-dapp/doctor-checks.js +0 -33
  68. package/dist/adapters/web-dapp/launch.js +0 -87
  69. package/dist/adapters/web-dapp/overlay.js +0 -21
  70. package/dist/adapters/web-dapp/readiness.js +0 -10
  71. package/dist/adapters/web-dapp/stop.js +0 -20
  72. package/dist/async.js +0 -19
  73. package/dist/cli.js +0 -202
@@ -1,11 +1,17 @@
1
+ import { createRequire } from "node:module";
1
2
  import path from "node:path";
2
3
  import { promisify } from "node:util";
3
4
  import { gzip } from "node:zlib";
4
5
  import { getAdapterSurface } from "./adapters/surface.js";
5
- import { createExtensionPerformanceBackend } from "./adapters/extension/performance-observer.js";
6
- import { TRACE_KINDS } from "./adapters/performance/cdp-trace.js";
7
- import { summarizeFrames } from "@farmslot/adapter-rn";
8
- import { summarizeJavaScriptTasks } from "./adapters/performance/js-task-metrics.js";
6
+ import { resolveExtensionId } from "./adapters/extension/extension-id.js";
7
+ import {
8
+ TRACE_KINDS,
9
+ TRACE_MARKER_PREFIX
10
+ } from "./adapters/performance/trace-kinds.js";
11
+ import {
12
+ summarizeFrames,
13
+ summarizeJavaScriptTasks
14
+ } from "@farmslot/recipe-runner/runtime/cdp-trace";
9
15
  import {
10
16
  indexArtifactManifest,
11
17
  readContainedJsonArtifact,
@@ -512,7 +518,24 @@ function unavailableCapture(session, window, reason) {
512
518
  );
513
519
  }
514
520
  async function createExtensionPerformanceObserver(target, env) {
515
- return createExtensionPerformanceBackend(extensionCdpPort(env), target);
521
+ const cdpPort = extensionCdpPort(env);
522
+ const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
523
+ if (!extensionId) {
524
+ throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
525
+ }
526
+ const { createExtensionPerformanceBackend } = createRequire(import.meta.url)(
527
+ "@farmslot/adapter-web/performance-observer"
528
+ );
529
+ return createExtensionPerformanceBackend({
530
+ cdpPort,
531
+ extensionId,
532
+ uiPaths: ["/home.html", "/sidepanel.html"],
533
+ kind: TRACE_KINDS.extension,
534
+ platform: "extension",
535
+ markerPrefix: TRACE_MARKER_PREFIX,
536
+ connectTimeoutMs: 1e4,
537
+ commandTimeoutMs: 1e4
538
+ });
516
539
  }
517
540
  function extensionCdpPort(env) {
518
541
  const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
@@ -67,9 +67,11 @@ in the registry, so a platform registered the same way is reached by the
67
67
  commands that take `--adapter` with no edit to shared code
68
68
  (`tests/fixtures/example-adapter.ts`); `status` detects the adapter from the
69
69
  checkout.
70
- The CLI contract (`src/command-contract.ts`) still lists the four built-ins for
71
- `--adapter`, so a new platform reaches `bin/mm-harness` once a plugin loader
72
- registers it and extends that list.
70
+ The public command table (`src/command-contract.ts`) reads the `--adapter`
71
+ choices when a command is validated: the registered adapters, then the adapters
72
+ the libraries on `RECIPE_LIBRARY_PATH` and the command's `--library` entries
73
+ declare. A new platform or library plugin reaches `bin/mm-harness` with no edit
74
+ to the table.
73
75
 
74
76
  Extend `AdapterSurface` when a platform owns the behavior; members a platform
75
77
  does not need are optional, and Core reports headless/N/A explicitly. Put each
@@ -82,7 +84,7 @@ command-local platform ladders for surface-owned behavior:
82
84
  - `run`/`call` come from `@farmslot/recipe-cli/harness` and read `run`,
83
85
  `recording`, `diagnostics`, `observation` and `sourceFingerprint`;
84
86
  mm-harness hands them its engine (`src/engine.ts`) and its device targeting
85
- and plan steps (`src/cli.ts`);
87
+ and plan steps (`src/mm-harness-cli.ts`);
86
88
  - the runner reads `actions` (manifest, bundled semantic actions, live adapter
87
89
  scripts, `cdp.target`, the `ui.*` transport, adapter wrappers, input checks);
88
90
  - `install`/`verify`/`cleanup` read `harness`, `agentic-runtime.json` reads
@@ -94,7 +94,7 @@ These are real farm bugs. The goldens freeze the current rejections, so fixing e
94
94
 
95
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
- `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.
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. The family copies one pinned commit of it (`GOLDENS_TERMINAL_LIBRARY_REV` in `goldens-terminal.test.sh`) into its sandbox with `git archive` and puts it on `RECIPE_LIBRARY_PATH`, so the goldens don't move when the checkout does. The checkout must contain that commit (`git fetch`); moving the pin and regenerating the goldens is one change. 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
98
 
99
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.
100
100
 
@@ -74,7 +74,7 @@ Captures are normalized (paths, versions, pids, timestamps, durations, epoch num
74
74
 
75
75
  What the normaliser deliberately masks, and so cannot catch: any semver under a key ending in `version` other than the harness's own, numbers under keys ending in `Ms`/`Seconds`/`duration`/`elapsed` (so a changed timeout value), epoch-millisecond numbers, and the `volatile` paths (a new product warning during a live run). Each varies run to run on real slots; the goldens freeze the static values.
76
76
 
77
- Machine rules: on macwork, headless only and no focus stealing (`TERMINAL_HEADLESS=1` is in the matrix). Headful runs and the Extension wallet popup fail while macwork's screen is locked (`Page.captureScreenshot` timeout). Mobile and the Extension run on free macpro or mini slots, never mini-mm-3. The differential stops and cleans the slot runtime it used; reinstall the farm's overlay afterwards if the slot is handed back to the farm.
77
+ Machine rules: on macwork, headless only and no focus stealing (`TERMINAL_HEADLESS=1` is in the matrix). A headful `run`/`call` (Extension, including its wallet popup, or a headful web-dapp) can't proceed while macwork's screen is locked: it refuses with `SESSION_LOCKED` at its runtime check (it used to stall until a `Page.captureScreenshot` timeout). Mobile and the Extension run on free macpro or mini slots, never mini-mm-3. The differential stops and cleans the slot runtime it used; reinstall the farm's overlay afterwards if the slot is handed back to the farm.
78
78
 
79
79
  ## 3. Release smoke
80
80
 
@@ -904,6 +904,16 @@ export async function runAdapter(callback) {
904
904
  // Surface the failure; still tear down so a half-open WebSocket from a
905
905
  // failed write doesn't leave the process wedged open.
906
906
  process.stderr.write(`[core/perps] adapter failed: ${fmtError(error)}\n`);
907
+ // A module the checkout cannot resolve is its environment (a partial
908
+ // workspace build), not app logic: say so on its own line, with the
909
+ // build step, for the runner's environment classification to match.
910
+ if (error?.code === 'ERR_MODULE_NOT_FOUND' || error?.code === 'MODULE_NOT_FOUND') {
911
+ const root = input.context?.projectRoot ?? process.cwd();
912
+ const missing = error.url ?? String(error.message ?? '').split('\n')[0];
913
+ process.stderr.write(
914
+ `[core/perps] environment: cannot resolve ${missing}\n Next: cd '${root.replace(/'/gu, `'\\''`)}' && yarn build\n`,
915
+ );
916
+ }
907
917
  }
908
918
  } finally {
909
919
  await disconnectController(exitCode);
@@ -100,6 +100,23 @@ async function unlockProfile(page, credentials, timeoutMs) {
100
100
  await waitForSelectorToDisappear(page, SELECTORS.unlockPassword, timeoutMs);
101
101
  }
102
102
 
103
+ // A locked profile is unlocked before the account proof. A fresh profile can
104
+ // also lock right after onboarding while the sidepanel attaches, stranding the
105
+ // proof on #/unlock: that relock is recovered once with the fixture password and
106
+ // reported as relockRecovered; a further lock fails with the route error.
107
+ async function ensureUnlockedSelectedAccount(page, credentials, timeoutMs) {
108
+ if (await hasVisibleSelector(page, SELECTORS.unlockPassword)) await unlockProfile(page, credentials, timeoutMs);
109
+ try {
110
+ const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
111
+ return { ...visibleSession, relockRecovered: false };
112
+ } catch (error) {
113
+ if (!(await hasVisibleSelector(page, SELECTORS.unlockPassword))) throw error;
114
+ await unlockProfile(page, credentials, timeoutMs);
115
+ const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
116
+ return { ...visibleSession, relockRecovered: true };
117
+ }
118
+ }
119
+
103
120
  async function importThroughUi(page, input, timeoutMs) {
104
121
  const credentials = await resolveWalletImportCredentials(input);
105
122
  const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
@@ -149,7 +166,7 @@ async function importThroughUi(page, input, timeoutMs) {
149
166
  await waitForSelector(page, SELECTORS.complete, timeoutMs);
150
167
  await page.click(SELECTORS.complete);
151
168
 
152
- const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
169
+ const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
153
170
  return { credentials, visibleSession, metametricsEnabled };
154
171
  }
155
172
 
@@ -168,9 +185,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
168
185
  if (!onboarding) {
169
186
  const credentials = await resolveWalletImportCredentials(input);
170
187
  const completionTriggered = await dismissVisibleOnboardingCompletion(page);
171
- const locked = await hasVisibleSelector(page, SELECTORS.unlockPassword);
172
- if (locked) await unlockProfile(page, credentials, timeoutMs);
173
- const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
188
+ const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
174
189
  return {
175
190
  action: input.action,
176
191
  method: 'ui-visible-session',
@@ -191,6 +206,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
191
206
  selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
192
207
  selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
193
208
  selectionPerformed: imported.visibleSession.selectionPerformed,
209
+ relockRecovered: imported.visibleSession.relockRecovered,
194
210
  completedOnboarding: true,
195
211
  metametricsEnabled: imported.metametricsEnabled,
196
212
  interests: [],
@@ -213,6 +229,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
213
229
  selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
214
230
  selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
215
231
  selectionPerformed: imported.visibleSession.selectionPerformed,
232
+ relockRecovered: imported.visibleSession.relockRecovered,
216
233
  completedOnboarding: true,
217
234
  metametricsEnabled: imported.metametricsEnabled,
218
235
  interests: [],
@@ -44,7 +44,7 @@ async function currentRoute(page, deadline) {
44
44
  return route.replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200);
45
45
  }
46
46
 
47
- async function routeError(page, message, deadline) {
47
+ async function routeError(page, message, deadline, cause) {
48
48
  let route = '<deadline-expired>';
49
49
  if (Date.now() < deadline) {
50
50
  try {
@@ -53,7 +53,35 @@ async function routeError(page, message, deadline) {
53
53
  route = '<unavailable>';
54
54
  }
55
55
  }
56
- return new Error(`${message} (route ${JSON.stringify(route)}).`);
56
+ const detail = cause
57
+ ? `; last input error: ${String(cause.message ?? cause).replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200)}`
58
+ : '';
59
+ return new Error(`${message} (route ${JSON.stringify(route)})${detail}.`);
60
+ }
61
+
62
+ // Entry can fail transiently while the account list settles after onboarding,
63
+ // so it is retried in place until the shared deadline; leaving the list (a
64
+ // lock, a redirect) fails at once with the last input error. Callers still
65
+ // assert the rendered result, so a retry can never pass a wrong account.
66
+ async function enterAccountSearch(page, value, deadline, message) {
67
+ let lastError;
68
+ while (Date.now() <= deadline) {
69
+ try {
70
+ await withinDeadline(page.setInput(ACCOUNT_SEARCH, value), deadline);
71
+ return;
72
+ } catch (error) {
73
+ lastError = error;
74
+ }
75
+ let route;
76
+ try {
77
+ route = await currentRoute(page, deadline);
78
+ } catch {
79
+ break;
80
+ }
81
+ if (route !== '#/account-list') break;
82
+ await new Promise((resolve) => setTimeout(resolve, 100));
83
+ }
84
+ throw await routeError(page, message, deadline, lastError);
57
85
  }
58
86
 
59
87
  async function visibleAccountMatch(page, deadline) {
@@ -140,11 +168,7 @@ async function openAccountList(page, deadline) {
140
168
 
141
169
  async function proveAddressSearchControl(page, expectedAddress, deadline) {
142
170
  const probeAddress = accountProbeAddress(expectedAddress);
143
- try {
144
- await withinDeadline(page.setInput(ACCOUNT_SEARCH, probeAddress), deadline);
145
- } catch {
146
- throw await routeError(page, 'Visible wallet account search could not enter the alternate-address control', deadline);
147
- }
171
+ await enterAccountSearch(page, probeAddress, deadline, 'Visible wallet account search could not enter the alternate-address control');
148
172
  let consecutiveEmpty = 0;
149
173
  while (Date.now() <= deadline) {
150
174
  let observed;
@@ -166,11 +190,7 @@ async function proveAddressSearchControl(page, expectedAddress, deadline) {
166
190
 
167
191
  async function queryExpectedAccount(page, expectedAddress, deadline, requireSelected) {
168
192
  await proveAddressSearchControl(page, expectedAddress, deadline);
169
- try {
170
- await withinDeadline(page.setInput(ACCOUNT_SEARCH, expectedAddress), deadline);
171
- } catch {
172
- throw await routeError(page, 'Visible wallet account search could not enter the derived release identity', deadline);
173
- }
193
+ await enterAccountSearch(page, expectedAddress, deadline, 'Visible wallet account search could not enter the derived release identity');
174
194
  let observed;
175
195
  while (Date.now() <= deadline) {
176
196
  try {
@@ -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/web-dapp/lib/processes.mjs, so the web-dapp stop path finds it.
18
+ // @farmslot/adapter-web/web-dapp, 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}`;
@@ -4,7 +4,7 @@
4
4
  // start forgets the app's stored agent key and the extension's dapp permission.
5
5
 
6
6
  import { appPort, cdpPort, isDirectRun, resolveSigner, runAdapter, webDappPolicy } from '../platform/runtime.mjs';
7
- import { launchWebDappBrowser } from '../../../../adapters/web-dapp/launch.mjs';
7
+ import { launchWebDappBrowser } from '@farmslot/adapter-web/web-dapp';
8
8
 
9
9
  export async function launch(input) {
10
10
  const node = input.node ?? {};
@@ -0,0 +1,18 @@
1
+ // Helpers from @farmslot/adapter-web's web-dapp lib that its public
2
+ // `./web-dapp` entry does not export, loaded from the installed package, never
3
+ // copied: the display and HUD helpers the actions share with the launcher and
4
+ // wallet host (one hud.json format), and the fixture and readiness helpers the
5
+ // MetaMask signer module uses. The signer module cannot import `./web-dapp`
6
+ // itself: the launch leaf loads it while that entry is still evaluating.
7
+
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+ import { pathToFileURL } from 'node:url';
11
+
12
+ const packageRoot = path.dirname(createRequire(import.meta.url).resolve('@farmslot/adapter-web/package.json'));
13
+ const lib = (name) => import(pathToFileURL(path.join(packageRoot, 'src/web-dapp/lib', name)).href);
14
+
15
+ export const { resolveHeadless, resolveSlowMo } = await lib('display.mjs');
16
+ export const { HUD_ELEMENT, clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } = await lib('hud.mjs');
17
+ export const { fixtureEntry } = await lib('runtime.mjs');
18
+ export const { check: readinessCheck } = await lib('readiness.mjs');
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { isAppUrl } from '@farmslot/adapter-web/origin';
7
7
 
8
- import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from '../../../../adapters/web-dapp/lib/hud.mjs';
8
+ import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from './adapter-web-lib.mjs';
9
9
  import { appOrigin, cdpPort, readBrowserState, webDappRuntimeDir } from './runtime.mjs';
10
10
  import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
11
11
 
@@ -7,10 +7,9 @@
7
7
  // so it stays the window's active tab and keeps painting without it.
8
8
 
9
9
  import { isAppUrl } from '@farmslot/adapter-web/origin';
10
+ import { cdpListenerPids } from '@farmslot/adapter-web/web-dapp';
10
11
 
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';
12
+ import { HUD_ELEMENT, resolveSlowMo } from './adapter-web-lib.mjs';
14
13
  import { appOrigin, cdpPort, readBrowserState } from './runtime.mjs';
15
14
  import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
16
15
 
@@ -1,186 +1,37 @@
1
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.
2
+ // browser state and wallet-fixture accounts, from @farmslot/adapter-web; plus
3
+ // the live-action plumbing (input, direct run, output). Key material is only
4
+ // ever returned as a viem account object; callers must never log or persist it.
4
5
 
5
- import { realpathSync } from 'node:fs';
6
6
  import { readFile, writeFile } from 'node:fs/promises';
7
- import path from 'node:path';
8
7
  import { pathToFileURL } from 'node:url';
9
8
 
10
- import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
11
-
12
- import { recipeRuntimeDir } from '@farmslot/recipe-cli/harness';
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 };
17
-
18
- export const SIGNER_MODES = Object.freeze(['extension', 'injected']);
19
- export const DEFAULT_ACCOUNT = 'dev1';
20
-
21
- // The checkout's canonical path: a symlinked or /tmp-vs-/private/tmp spelling
22
- // of the same slot must name the same runtime dir, profile and pid owners.
23
- export function canonicalPath(target) {
24
- const resolved = path.resolve(target);
25
- try {
26
- return realpathSync(resolved);
27
- } catch {
28
- return resolved;
29
- }
30
- }
31
-
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);
35
- }
36
-
37
- export function webDappRuntimePath(projectRoot, ...segments) {
38
- return path.join(webDappRuntimeDir(projectRoot), ...segments);
39
- }
40
-
41
- function portFrom(raw, label) {
42
- if (raw === undefined || raw === null || raw === '') return undefined;
43
- const port = Number(raw);
44
- if (!Number.isInteger(port) || port <= 0 || port > 65535) {
45
- throw new Error(`${label} must be a TCP port, got ${JSON.stringify(raw)}.`);
46
- }
47
- return port;
48
- }
49
-
50
- // The slot's Next.js dev server. Farmslot passes it as --watcher-port {{port}};
51
- // TERMINAL_APP_PORT is the explicit override.
52
- export function appPort(input = {}, env = process.env) {
53
- const port =
54
- portFrom(input.node?.app_port, 'app_port') ??
55
- portFrom(env.TERMINAL_APP_PORT, 'TERMINAL_APP_PORT') ??
56
- portFrom(env.RECIPE_WATCHER_PORT, 'RECIPE_WATCHER_PORT') ??
57
- portFrom(env.WATCHER_PORT, 'WATCHER_PORT');
58
- if (!port) {
59
- throw new Error(
60
- `${webDappPolicy().adapterId} adapter requires the slot dev-server port.\n` +
61
- 'Next: pass --watcher-port <port> (Farmslot: {{port}}) or set TERMINAL_APP_PORT.',
62
- );
63
- }
64
- return port;
65
- }
66
-
67
- export function appOrigin(input = {}, env = process.env) {
68
- return `http://localhost:${appPort(input, env)}`;
69
- }
70
-
71
- export function cdpPort(input = {}, env = process.env) {
72
- const port =
73
- portFrom(input.node?.cdp_port, 'cdp_port') ??
74
- portFrom(env.RECIPE_CDP_PORT, 'RECIPE_CDP_PORT') ??
75
- portFrom(env.CDP_PORT, 'CDP_PORT');
76
- if (!port) {
77
- throw new Error(
78
- `${webDappPolicy().adapterId} adapter requires a per-slot browser CDP port.\n` +
79
- 'Next: pass --cdp-port <port> (Farmslot: {{cdp_port}}).',
80
- );
81
- }
82
- return port;
83
- }
84
-
85
- export async function readJsonFile(file) {
86
- try {
87
- return JSON.parse(await readFile(file, 'utf8'));
88
- } catch (error) {
89
- if (error?.code === 'ENOENT') return null;
90
- throw error;
91
- }
92
- }
93
-
94
- export async function writePrivateJson(file, value) {
95
- await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
96
- }
97
-
98
- export async function readBrowserState(projectRoot) {
99
- return readJsonFile(webDappRuntimePath(projectRoot, 'browser.json'));
100
- }
101
-
102
- export function resolveSigner(raw) {
103
- const signer = raw == null || raw === '' ? 'extension' : String(raw);
104
- if (!SIGNER_MODES.includes(signer)) {
105
- throw new Error(`signer must be one of ${SIGNER_MODES.join(' | ')}, got ${JSON.stringify(raw)}.`);
106
- }
107
- return signer;
108
- }
109
-
110
- // Account name precedence: node.account_name → node.account (when not an
111
- // address) → the running browser's account → dev1.
112
- export function accountName(input = {}, browserState = null) {
113
- const explicit = input.node?.account_name ?? input.node?.account;
114
- if (typeof explicit === 'string' && explicit.trim() && !/^0x[0-9a-fA-F]{40}$/u.test(explicit.trim())) {
115
- return explicit.trim();
116
- }
117
- return browserState?.account?.name ?? DEFAULT_ACCOUNT;
118
- }
119
-
120
- export function fixturePath(projectRoot, env = process.env) {
121
- return env.RECIPE_WALLET_FIXTURE ? path.resolve(env.RECIPE_WALLET_FIXTURE) : walletFixturePath(projectRoot);
122
- }
123
-
124
- export async function loadWalletFixture(projectRoot, env = process.env) {
125
- const file = fixturePath(projectRoot, env);
126
- const fixture = await readJsonFile(file);
127
- if (!fixture) {
128
- throw new Error(
129
- `wallet fixture not found at ${file}.\n` +
130
- `Next: mm-harness install --adapter ${webDappPolicy().adapterId} --target <checkout> with RECIPE_WALLET_FIXTURE set, or copy the fixture there (0600).`,
131
- );
132
- }
133
- if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
134
- throw new Error(`wallet fixture at ${file} has no accounts.`);
135
- }
136
- return { file, fixture };
137
- }
138
-
139
- export function fixtureEntry(fixture, name) {
140
- const entry = fixture.accounts.find((account) => account?.name === name);
141
- if (!entry) {
142
- const names = fixture.accounts.map((account) => account?.name).filter(Boolean).join(', ');
143
- throw new Error(`wallet fixture has no account named "${name}". Available: ${names}.`);
144
- }
145
- if (entry.type !== 'mnemonic' && entry.type !== 'privateKey') {
146
- throw new Error(`wallet fixture account "${name}" must be mnemonic or privateKey, got "${entry.type}".`);
147
- }
148
- if (typeof entry.value !== 'string' || !entry.value.trim()) {
149
- throw new Error(`wallet fixture account "${name}" has no value.`);
150
- }
151
- return entry;
152
- }
153
-
154
- // Mnemonics use BIP-44 index 0, the MetaMask default and the derivation the
155
- // Extension fixture flow imports.
156
- export function viemAccountFromEntry(entry) {
157
- if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex: 0 });
158
- const raw = entry.value.trim();
159
- const key = raw.startsWith('0x') ? raw : `0x${raw}`;
160
- if (!/^0x[0-9a-fA-F]{64}$/u.test(key)) {
161
- throw new Error(`wallet fixture privateKey account "${entry.name}" is not a 32-byte hex key.`);
162
- }
163
- return privateKeyToAccount(key);
164
- }
165
-
166
- export async function fixtureAccount(projectRoot, name, env = process.env) {
167
- const { fixture } = await loadWalletFixture(projectRoot, env);
168
- return viemAccountFromEntry(fixtureEntry(fixture, name));
169
- }
170
-
171
- // Address-only view of the account a node targets: an explicit address wins
172
- // (read-only use), otherwise the fixture account is derived.
173
- export async function resolveAccountAddress(input, env = process.env) {
174
- const raw = input.node?.account;
175
- if (typeof raw === 'string' && /^0x[0-9a-fA-F]{40}$/u.test(raw.trim())) {
176
- return { name: null, address: raw.trim() };
177
- }
178
- const projectRoot = input.context?.projectRoot;
179
- const browserState = projectRoot ? await readBrowserState(projectRoot) : null;
180
- const name = accountName(input, browserState);
181
- const account = await fixtureAccount(projectRoot, name, env);
182
- return { name, address: account.address };
183
- }
9
+ import { webDappAdapterId } from '@farmslot/adapter-web/web-dapp';
10
+
11
+ export {
12
+ accountName,
13
+ appOrigin,
14
+ appPort,
15
+ canonicalPath,
16
+ cdpPort,
17
+ DEFAULT_ACCOUNT,
18
+ fixtureAccount,
19
+ fixtureEntry,
20
+ fixturePath,
21
+ loadWalletFixture,
22
+ POLICY_ENV,
23
+ readBrowserState,
24
+ readJsonFile,
25
+ resolveAccountAddress,
26
+ resolveSigner,
27
+ SIGNER_MODES,
28
+ viemAccountFromEntry,
29
+ webDappAdapterId,
30
+ webDappPolicy,
31
+ webDappRuntimeDir,
32
+ webDappRuntimePath,
33
+ writePrivateJson,
34
+ } from '@farmslot/adapter-web/web-dapp';
184
35
 
185
36
  export async function loadInput() {
186
37
  const inputPath = process.argv[2] || process.env.METAMASK_RECIPE_ADAPTER_INPUT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.82.1",
3
+ "version": "0.84.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,14 +21,15 @@
21
21
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
22
22
  },
23
23
  "dependencies": {
24
- "@farmslot/adapter-rn": "0.17.1",
25
- "@farmslot/adapter-sdk": "0.5.0",
26
- "@farmslot/adapter-web": "0.3.1",
24
+ "@farmslot/adapter-node": "0.1.4",
25
+ "@farmslot/adapter-rn": "0.19.1",
26
+ "@farmslot/adapter-sdk": "0.7.1",
27
+ "@farmslot/adapter-web": "0.6.2",
27
28
  "@farmslot/agent-runtime": "0.17.0",
28
29
  "@farmslot/handoff": "^0.3.1",
29
30
  "@farmslot/protocol": "0.34.0",
30
- "@farmslot/recipe-cli": "0.6.0",
31
- "@farmslot/recipe-runner": "0.25.0",
31
+ "@farmslot/recipe-cli": "0.10.0",
32
+ "@farmslot/recipe-runner": "0.27.0",
32
33
  "agent-device": "0.19.3",
33
34
  "commander": "^12.0.0",
34
35
  "esbuild": "0.28.1",
@@ -45,7 +45,7 @@ _mmh_flags_for() {
45
45
  execution-template) printf '%s' "--dir --domain-dir --project-worker --project-name --package-templates --package-id --flow --run-mode --platform --domain --id --provenance --include-shadowed --no-include-shadowed --title --force --json" ;;
46
46
  recipe-quality) printf '%s' "--input --output --recipe --task --evidence --params --adapter --library --advisor --advisor-model --advisor-stages --json" ;;
47
47
  actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
48
- call) printf '%s' "--list --domain --source --sort --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
48
+ call) printf '%s' "--list --domain --source --sort --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
49
49
  run) printf '%s' "--list --domain --source --sort --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
50
50
  doctor) printf '%s' "--adapter --target --fix --json" ;;
51
51
  install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
- // cleanup.mjs — `mm-harness cleanup --adapter <web-dapp adapter>`: stop the slot
3
- // browser and wallet host, then remove the slot's runtime state and the installed
4
- // wallet fixture. Browser profiles are kept unless --reset-profile.
5
- //
6
- // Inputs: --target <checkout> [--cdp-port <port>] [--reset-profile]
7
- // Outputs: JSON summary on stdout. Exit 0 cleaned; 1 the CDP port stayed busy.
8
- // Never touches: product source files, the Next.js dev server.
9
-
10
- import { existsSync, readdirSync, rmSync } from 'node:fs';
11
- import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- import { recipeHarnessPath } from '@farmslot/recipe-cli/harness';
15
- import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
16
- import { webDappPolicy, webDappRuntimeDir } from '../../library/actions/web-dapp/platform/runtime.mjs';
17
- import { stopConsoleCollector, stopWebDappBrowser } from './lib/processes.mjs';
18
-
19
- const usage = 'Usage: cleanup.mjs --target <checkout> [--cdp-port <port>] [--reset-profile]';
20
-
21
- export async function cleanupWebDappRuntime({ target, cdpPort, resetProfile = false }) {
22
- const root = path.resolve(target);
23
- const stop = await stopWebDappBrowser(root, { cdpPort });
24
- await stopConsoleCollector(root);
25
- const runtime = webDappRuntimeDir(root);
26
- const removed = [];
27
- if (existsSync(runtime)) {
28
- for (const entry of readdirSync(runtime)) {
29
- if (entry.startsWith('profile-') && !resetProfile) continue;
30
- rmSync(path.join(runtime, entry), { recursive: true, force: true });
31
- removed.push(entry);
32
- }
33
- }
34
- const overlay = recipeHarnessPath(root, webDappPolicy().adapterId);
35
- if (existsSync(overlay)) {
36
- rmSync(overlay, { recursive: true, force: true });
37
- removed.push(path.relative(root, overlay));
38
- }
39
- const fixture = walletFixturePath(root);
40
- if (existsSync(fixture)) {
41
- rmSync(fixture, { force: true });
42
- removed.push(path.relative(root, fixture));
43
- }
44
- return { status: 'pass', target: root, stopped: stop.stopped, cdpPortFree: stop.cdpPortFree, removed };
45
- }
46
-
47
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
48
- const argv = process.argv.slice(2);
49
- if (argv.includes('--help') || argv.includes('-h')) {
50
- process.stdout.write(`${usage}\n`);
51
- process.exit(0);
52
- }
53
- const value = (flag) => {
54
- const index = argv.indexOf(flag);
55
- return index >= 0 ? argv[index + 1] : undefined;
56
- };
57
- const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
58
- try {
59
- const result = await cleanupWebDappRuntime({
60
- target: value('--target') ?? process.cwd(),
61
- cdpPort: rawPort ? Number(rawPort) : undefined,
62
- resetProfile: argv.includes('--reset-profile'),
63
- });
64
- process.stdout.write(`${JSON.stringify(result)}\n`);
65
- } catch (error) {
66
- process.stdout.write(`${JSON.stringify({ status: 'fail', error: error.message })}\n`);
67
- process.exit(1);
68
- }
69
- }