@deeeed/metamask-harness 0.77.0 → 0.78.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 (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +8 -5
  3. package/adapters/extension/build-handoff.cjs +2 -2
  4. package/adapters/extension/ensure-browser.sh +10 -12
  5. package/adapters/extension/inject.mjs +10 -7
  6. package/adapters/extension/launch-browser.cjs +7 -10
  7. package/adapters/extension/live.sh +1 -2
  8. package/adapters/extension/reattach.sh +3 -3
  9. package/adapters/extension/sidepanel-toggle.sh +4 -4
  10. package/adapters/extension/wallet-fixture-state.cjs +2 -2
  11. package/adapters/manifest.json +0 -24
  12. package/adapters/shared/harness-source-fingerprint.mjs +16 -2
  13. package/adapters/terminal/launch.mjs +2 -2
  14. package/adapters/terminal/wallet-host.mjs +1 -1
  15. package/dist/adapters/extension/doctor-checks.js +2 -2
  16. package/dist/adapters/extension/launched-browser.js +1 -1
  17. package/dist/adapters/extension/runtime.js +4 -4
  18. package/dist/adapters/extension/validation-process-ownership.js +1 -1
  19. package/dist/commands/launch/extension.js +1 -1
  20. package/docs/cli-contract.md +6 -17
  21. package/library/actions/extension/platform/cdp.mjs +5 -7
  22. package/library/actions/terminal/platform/page.mjs +2 -2
  23. package/package.json +2 -1
  24. package/adapters/extension/lib/chrome-args.cjs +0 -365
  25. package/adapters/extension/lib/extension-id.cjs +0 -36
  26. package/adapters/extension/lib/macos-focus.cjs +0 -108
  27. package/adapters/extension/lib/playwright-cdp.cjs +0 -50
  28. package/adapters/extension/lib/validation-launch-supervisor.cjs +0 -292
  29. package/adapters/extension/lib/validation-process-ownership.cjs +0 -125
  30. package/adapters/shared/browser-cdp.cjs +0 -542
  31. package/adapters/shared/browser-resolver.cjs +0 -1007
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.78.0 - 2026-10-05
6
+
7
+ ### Changed
8
+
9
+ - The browser process and CDP helpers moved to `@farmslot/adapter-web` (new dependency): `browser-resolver`, `browser-cdp`, `chrome-args`, `extension-id`, `macos-focus`, `playwright-cdp`, `validation-process-ownership` and `validation-launch-supervisor`. Their old paths under `adapters/shared/` and `adapters/extension/lib/` are gone; adapters, library actions, the Terminal launcher and `src/` require the package. Behaviour is unchanged.
10
+ - **Upgrade note: stop running slot browsers before upgrading**: run `mm-harness stop --adapter extension --target <checkout>` once for each Extension checkout with a running browser. The browser identity names are Farmslot's now and the old runtime nonce is no longer accepted, so a browser started by an earlier version may not be recognised as owned (one found by its slot profile still is): `--farmslot-runtime-nonce=`, `.farmslot-detached-launch-unproven`, `/tmp/farmslot-browser-validation-<uid>`, and the probe cache `~/.cache/farmslot/browser-probe.json` (the old `~/.cache/mm-harness/browser-probe.json` and its lock directories are no longer read and can be deleted). The focus knobs are `FARMSLOT_FOCUS_HOLD` and `FARMSLOT_FOCUS_BROWSER` (were `MM_HARNESS_FOCUS_HOLD` / `MM_HARNESS_FOCUS_BROWSER`).
11
+ - The Extension overlay vendors `@farmslot/adapter-web` into `<harness>/scripts/node_modules/` instead of flattening copies into `scripts/lib/`, and the harness source fingerprint covers the package sources, so a package upgrade refreshes installed overlays.
12
+
5
13
  ## 0.77.0 - 2026-10-04
6
14
 
7
15
  ### Changed
package/README.md CHANGED
@@ -134,7 +134,7 @@ disabled, so the launch warns and records them with the run evidence.
134
134
 
135
135
  In `auto`, a launch with a new profile probe-launches Chrome for Testing the
136
136
  way the launcher starts it (headful, through Launch Services on macOS). The
137
- verdict lives in `~/.cache/mm-harness/browser-probe.json`: a crash is kept
137
+ verdict lives in `~/.cache/farmslot/browser-probe.json`: a crash is kept
138
138
  until the OS build or browser changes; a probe that only timed out is retried
139
139
  after 30 minutes. Concurrent launches wait for one probe per browser build
140
140
  (a `<cache>.<key>.locks/` directory of numbered tickets next to the cache; a
@@ -144,7 +144,7 @@ file one duplicate probe is possible). Tickets are never removed by the
144
144
  harness: each probe adds two small files, and a crashed holder's unreleased
145
145
  ticket stays (it no longer blocks once its process is gone). To reclaim the
146
146
  space, remove the lock directories while no launch or doctor is running:
147
- `rm -r ~/.cache/mm-harness/browser-probe.json.*.locks`.
147
+ `rm -r ~/.cache/farmslot/browser-probe.json.*.locks`.
148
148
  On a cold cache the probe can take up to about 80 seconds and
149
149
  shows a small (about 480×360) browser window; `doctor --adapter extension`
150
150
  runs the same probe.
@@ -205,8 +205,11 @@ On macOS the slot browser starts in the background with no startup window,
205
205
  and its first window opens over CDP as a background window. After the launch
206
206
  settles there is one focus check: only if the slot browser's own process is
207
207
  frontmost does the previously focused app get the front back, once
208
- (`MM_HARNESS_FOCUS_HOLD=0` turns this off). A click on the slot window right
209
- after launch can therefore be undone once.
208
+ (`FARMSLOT_FOCUS_HOLD=0` turns this off). A click on the slot window right
209
+ after launch can therefore be undone once. `FARMSLOT_FOCUS_HOLD` and
210
+ `FARMSLOT_FOCUS_BROWSER` belong to `@farmslot/adapter-web`; the settle delay
211
+ before that check, `MM_HARNESS_FOCUS_SETTLE_MS`, belongs to this harness's
212
+ launchers.
210
213
 
211
214
  ```bash
212
215
  # Roll back to Chrome for Testing with a fresh slot profile (a profile Google
@@ -214,7 +217,7 @@ after launch can therefore be undone once.
214
217
  # RECIPE_HARNESS_BROWSER=cft set for later launches too: on a plain auto launch
215
218
  # the profile follows Chrome for Testing only until a probe proves it dies.
216
219
  RECIPE_HARNESS_BROWSER=cft mm-harness fixtures set
217
- rm ~/.cache/mm-harness/browser-probe.json # forget probe verdicts
220
+ rm ~/.cache/farmslot/browser-probe.json # forget probe verdicts
218
221
  ```
219
222
 
220
223
  ### Remote feature flags
@@ -4,8 +4,8 @@
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const { spawnSync } = require('node:child_process');
7
- const { connectBrowserViaCdp, evaluatePageViaCdp } = require('./lib/playwright-cdp.cjs');
8
- const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = require('./lib/chrome-args.cjs');
7
+ const { connectBrowserViaCdp, evaluatePageViaCdp } = require('@farmslot/adapter-web/playwright-cdp');
8
+ const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = require('@farmslot/adapter-web/chrome-args');
9
9
  const { stampRuntimeTitles } = require('./stamp-runtime-title.cjs');
10
10
  const { carryHarnessPin } = require('./pin-remote-flags.cjs');
11
11
 
@@ -205,10 +205,10 @@ kill_profile_processes() {
205
205
 
206
206
  # Invalidate the old browser identity before signaling it. A concurrent quick
207
207
  # launch must fall through to process ownership while replacement is underway.
208
- node - "$SCRIPT_DIR/lib/chrome-args.cjs" "$AGENT_DIR" <<'NODE'
209
- const [modulePath, runtimeDir] = process.argv.slice(2);
208
+ node - "$SCRIPT_DIR" "$AGENT_DIR" <<'NODE'
209
+ const [scriptDir, runtimeDir] = process.argv.slice(2);
210
210
  try {
211
- require(modulePath).removeRuntimeIdentity(runtimeDir);
211
+ require('node:module').createRequire(`${scriptDir}/`)('@farmslot/adapter-web/chrome-args').removeRuntimeIdentity(runtimeDir);
212
212
  } catch (error) {
213
213
  const detail = error instanceof Error ? error.message : String(error);
214
214
  throw new Error(
@@ -292,6 +292,7 @@ trap - EXIT
292
292
  exec node -e "
293
293
  const path = require('path');
294
294
  const fs = require('fs');
295
+ const adapterWeb = (name) => require('module').createRequire(path.join('${SCRIPT_DIR}', 'package.json'))('@farmslot/adapter-web/' + name);
295
296
  const http = require('http');
296
297
  const { execFileSync } = require('child_process');
297
298
  let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
@@ -303,9 +304,9 @@ const {
303
304
  removeRuntimeIdentity,
304
305
  runtimeIdentityArgs,
305
306
  writeRuntimeIdentity,
306
- } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
307
- const { captureMacFrontmost, macBackgroundOpenArgs, restoreFrontmostIfOurs } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
308
- const { stopProfileProcessesSync } = require(path.join('${SCRIPT_DIR}', 'lib/validation-process-ownership.cjs'));
307
+ } = adapterWeb('chrome-args');
308
+ const { captureMacFrontmost, macBackgroundOpenArgs, restoreFrontmostIfOurs } = adapterWeb('macos-focus');
309
+ const { stopProfileProcessesSync } = adapterWeb('validation-process-ownership');
309
310
  const {
310
311
  CDP_LOAD_UNPACKED,
311
312
  HEADFUL,
@@ -315,10 +316,7 @@ const {
315
316
  waitForCdpOwner,
316
317
  playwrightChromiumExecutable,
317
318
  resolveBrowser,
318
- } = require([
319
- path.join('${SCRIPT_DIR}', 'lib/browser-resolver.cjs'),
320
- path.join('${SCRIPT_DIR}', '../shared/browser-resolver.cjs'),
321
- ].find((candidate) => fs.existsSync(candidate)));
319
+ } = adapterWeb('browser-resolver');
322
320
 
323
321
  const SLOT_ID = '${SLOT_ID}';
324
322
  const AGENT_DIR = '${AGENT_DIR}';
@@ -520,7 +518,7 @@ const releaseFocusHold = () => {
520
518
 
521
519
  // Set window title to slot ID (persists across SPA navigations)
522
520
  const { applyPersistentSlotTitle } = require(path.join('${SCRIPT_DIR}', 'lib/slot-title.cjs'));
523
- const { evaluatePageViaCdp } = require(path.join('${SCRIPT_DIR}', 'lib/playwright-cdp.cjs'));
521
+ const { evaluatePageViaCdp } = adapterWeb('playwright-cdp');
524
522
  await evaluatePageViaCdp(page, applyPersistentSlotTitle, SLOT_ID);
525
523
 
526
524
  // Wait for runtime state to settle, then unlock if needed.
@@ -573,7 +571,7 @@ const releaseFocusHold = () => {
573
571
  await p.close().catch(() => {});
574
572
  }
575
573
  }
576
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
574
+ if (process.env.FARMSLOT_FOCUS_BROWSER === '1') {
577
575
  await page.bringToFront().catch(() => {});
578
576
  } else {
579
577
  const settleMs = Number(process.env.MM_HARNESS_FOCUS_SETTLE_MS);
@@ -20,6 +20,7 @@
20
20
  // Never touches: product source files; anything outside the target's
21
21
  // harness dir and the target's .git/info/exclude.
22
22
  import fs from 'node:fs';
23
+ import { createRequire } from 'node:module';
23
24
  import path from 'node:path';
24
25
  import { spawnSync } from 'node:child_process';
25
26
  import process from 'node:process';
@@ -126,15 +127,9 @@ copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnes
126
127
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
127
128
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harnessDir, 'scripts/lib/tmux-viewer.sh'));
128
129
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
129
- copyFile(path.join(runnerDir, 'adapters/shared/browser-resolver.cjs'), path.join(harnessDir, 'scripts/lib/browser-resolver.cjs'));
130
- copyFile(path.join(runnerDir, 'adapters/shared/browser-cdp.cjs'), path.join(harnessDir, 'scripts/lib/browser-cdp.cjs'));
131
- copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
132
- copyFile(path.join(runnerDir, 'adapters/extension/lib/playwright-cdp.cjs'), path.join(harnessDir, 'scripts/lib/playwright-cdp.cjs'));
130
+ copyAdapterWeb(path.join(harnessDir, 'scripts/node_modules/@farmslot/adapter-web'));
133
131
  copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
134
- copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
135
- copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
136
132
  copyFile(path.join(runnerDir, 'adapters/extension/lib/product-config.cjs'), path.join(harnessDir, 'scripts/lib/product-config.cjs'));
137
- copyFile(path.join(runnerDir, 'adapters/extension/lib/validation-process-ownership.cjs'), path.join(harnessDir, 'scripts/lib/validation-process-ownership.cjs'));
138
133
  makeExecutableTree(path.join(harnessDir, 'scripts'));
139
134
  fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
140
135
 
@@ -205,6 +200,14 @@ function refuseSymlinkDestination(root, rel) {
205
200
  }
206
201
  }
207
202
 
203
+ // Overlay scripts run from the target checkout, which cannot resolve the runner's
204
+ // packages; vendor @farmslot/adapter-web where they resolve it (scripts/node_modules).
205
+ function copyAdapterWeb(dest) {
206
+ const packageJson = createRequire(path.join(runnerDir, 'package.json')).resolve('@farmslot/adapter-web/package.json');
207
+ copyFile(packageJson, path.join(dest, 'package.json'));
208
+ copyDir(path.join(path.dirname(packageJson), 'src'), path.join(dest, 'src'));
209
+ }
210
+
208
211
  function copyFile(src, dest) {
209
212
  fs.mkdirSync(path.dirname(dest), { recursive: true });
210
213
  fs.copyFileSync(src, dest);
@@ -27,14 +27,11 @@ const fs = require('node:fs');
27
27
  const os = require('node:os');
28
28
  const path = require('node:path');
29
29
  const { execFileSync, spawn, spawnSync } = require('node:child_process');
30
- const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
31
- // The installed overlay flattens shared helpers into scripts/lib/.
32
- const browserResolverPath = [
33
- path.join(__dirname, 'lib/browser-resolver.cjs'),
34
- path.join(__dirname, '../shared/browser-resolver.cjs'),
35
- ].find((candidate) => fs.existsSync(candidate));
30
+ const { extensionIdFromExtensionDir } = require('@farmslot/adapter-web/extension-id');
31
+ // The resolver CLI runs as a bounded child for CDP calls the launcher must not hang on.
32
+ const browserResolverPath = require.resolve('@farmslot/adapter-web/browser-resolver');
36
33
  const { CDP_LOAD_UNPACKED, describeBrowser, extensionLaunchArgs, waitForCdpOwner } = require(browserResolverPath);
37
- const { profileProcessPids, stopProfileProcessesSync } = require('./lib/validation-process-ownership.cjs');
34
+ const { profileProcessPids, stopProfileProcessesSync } = require('@farmslot/adapter-web/validation-process-ownership');
38
35
  const { acquireBuildHandoff } = require('./build-handoff.cjs');
39
36
  const {
40
37
  automationRuntimeArgs,
@@ -51,12 +48,12 @@ const {
51
48
  readValidationPortQuarantine,
52
49
  validationLaunchQuarantineError,
53
50
  writeRuntimeIdentity,
54
- } = require('./lib/chrome-args.cjs');
51
+ } = require('@farmslot/adapter-web/chrome-args');
55
52
  const {
56
53
  captureMacFrontmost,
57
54
  macBackgroundOpenArgs,
58
55
  restoreFrontmostIfOurs,
59
- } = require('./lib/macos-focus.cjs');
56
+ } = require('@farmslot/adapter-web/macos-focus');
60
57
 
61
58
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
62
59
  console.log(
@@ -138,7 +135,7 @@ if (args['stop-only'] !== undefined) {
138
135
 
139
136
  const requestedProfileQuarantined = hasDetachedLaunchUnproven(args.profile);
140
137
  const portQuarantine = readValidationPortQuarantine(cdpPort);
141
- const validationPortLease = process.env.MM_HARNESS_VALIDATION_PORT_LEASE;
138
+ const validationPortLease = process.env.FARMSLOT_VALIDATION_PORT_LEASE;
142
139
  const activeValidationLease = Boolean(
143
140
  portQuarantine?.profile === path.resolve(args.profile) &&
144
141
  portQuarantine.lease === validationPortLease &&
@@ -245,8 +245,7 @@ NODE
245
245
  # launcher knows how to load the extension and the run evidence names it.
246
246
  # launch-browser.cjs starts a macOS .app through Launch Services, headful,
247
247
  # so the probe does too.
248
- BROWSER_RESOLVER="$SCRIPT_DIR/lib/browser-resolver.cjs"
249
- [ -f "$BROWSER_RESOLVER" ] || BROWSER_RESOLVER="$SCRIPT_DIR/../shared/browser-resolver.cjs"
248
+ BROWSER_RESOLVER="$(node -e 'process.stdout.write(require("node:module").createRequire(`${process.argv[1]}/`).resolve("@farmslot/adapter-web/browser-resolver"))' "$SCRIPT_DIR")"
250
249
  # A preserved profile keeps the browser recorded for it (auto never flips a
251
250
  # profile between builds); the resolver bounds itself (probe + watchdog).
252
251
  CHROME_PROBE_LOG="$ARTIFACTS/logs/chrome-probe.log"
@@ -119,9 +119,9 @@ CDP_PORT="$CDP_PORT" EXT_ID="$EXT_ID" TARGET="$TARGET" SCRIPT_DIR="$SCRIPT_DIR"
119
119
  let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
120
120
  const fs = require('node:fs');
121
121
  const path = require('node:path');
122
- const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
122
+ const { extensionIdFromManifestFile } = require('node:module').createRequire(`${process.env.SCRIPT_DIR}/`)('@farmslot/adapter-web/extension-id');
123
123
  const { readSlotId, applyPersistentSlotTitle } = require(path.join(process.env.SCRIPT_DIR, 'lib/slot-title.cjs'));
124
- const { evaluatePageViaCdp } = require(path.join(process.env.SCRIPT_DIR, 'lib/playwright-cdp.cjs'));
124
+ const { evaluatePageViaCdp } = require('node:module').createRequire(`${process.env.SCRIPT_DIR}/`)('@farmslot/adapter-web/playwright-cdp');
125
125
 
126
126
  const port = process.env.CDP_PORT;
127
127
  const target = process.env.TARGET;
@@ -294,7 +294,7 @@ async function closePages(context, predicate) {
294
294
  if (!startUrl && (url.startsWith('http://') || url.startsWith('https://'))) return true;
295
295
  return false;
296
296
  });
297
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
297
+ if (process.env.FARMSLOT_FOCUS_BROWSER === '1') {
298
298
  await home.bringToFront().catch(() => {});
299
299
  }
300
300
  }
@@ -108,7 +108,7 @@ json_list() {
108
108
  }
109
109
 
110
110
  focus_browser() {
111
- [ "${MM_HARNESS_FOCUS_BROWSER:-}" = "1" ]
111
+ [ "${FARMSLOT_FOCUS_BROWSER:-}" = "1" ]
112
112
  }
113
113
 
114
114
  find_sidepanel_id() {
@@ -162,7 +162,7 @@ resolve_ext_id() {
162
162
  manifest_id="$(
163
163
  SCRIPT_DIR="$SCRIPT_DIR" REPO="$REPO" RUNTIME_DIR="$RUNTIME_DIR" RUNTIME_DIST_DIR="$RUNTIME_DIST_DIR" node <<'NODE' 2>/dev/null || true
164
164
  const path = require('node:path');
165
- const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
165
+ const { extensionIdFromManifestFile } = require('node:module').createRequire(`${process.env.SCRIPT_DIR}/`)('@farmslot/adapter-web/extension-id');
166
166
  const id = extensionIdFromManifestFile(path.join(process.env.REPO, process.env.RUNTIME_DIR, process.env.RUNTIME_DIST_DIR, 'manifest.json'));
167
167
  if (id) process.stdout.write(id);
168
168
  NODE
@@ -321,7 +321,7 @@ const path = require('node:path');
321
321
  const {
322
322
  connectBrowserViaCdp,
323
323
  evaluatePageViaCdp,
324
- } = require(path.join(process.env.SCRIPT_DIR, 'lib/playwright-cdp.cjs'));
324
+ } = require('node:module').createRequire(`${process.env.SCRIPT_DIR}/`)('@farmslot/adapter-web/playwright-cdp');
325
325
 
326
326
  async function clickVisibleSafeHit(page, selector, successSelector, timeoutMs) {
327
327
  const deadline = Date.now() + timeoutMs;
@@ -371,7 +371,7 @@ async function clickVisibleSafeHit(page, selector, successSelector, timeoutMs) {
371
371
  waitUntil: 'domcontentloaded',
372
372
  timeout: 15000,
373
373
  });
374
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
374
+ if (process.env.FARMSLOT_FOCUS_BROWSER === '1') {
375
375
  await page.bringToFront();
376
376
  }
377
377
  await clickVisibleSafeHit(
@@ -20,8 +20,8 @@ const crypto = require('node:crypto');
20
20
  const fs = require('node:fs');
21
21
  const http = require('node:http');
22
22
  const path = require('node:path');
23
- const { extensionIdFromManifestKey } = require('./lib/extension-id.cjs');
24
- const { evaluatePageViaCdp } = require('./lib/playwright-cdp.cjs');
23
+ const { extensionIdFromManifestKey } = require('@farmslot/adapter-web/extension-id');
24
+ const { evaluatePageViaCdp } = require('@farmslot/adapter-web/playwright-cdp');
25
25
 
26
26
  const EOA_METHODS = [
27
27
  'personal_sign',
@@ -282,14 +282,6 @@
282
282
  "inputs": "--cdp-port --chrome-bin --profile --extension-dir --chrome-log --chrome-pid [--browser-resolution]",
283
283
  "outputs": "pid file + append-log + <runtime>/browser-resolution.json; exit 0 / non-zero"
284
284
  },
285
- {
286
- "id": "extension/validation-process-ownership",
287
- "entry": "adapters/extension/lib/validation-process-ownership.cjs",
288
- "kind": "lib",
289
- "purpose": "Resolve browser processes owned by one isolated Extension validation profile.",
290
- "inputs": "profile path",
291
- "outputs": "owned process identifiers"
292
- },
293
285
  {
294
286
  "id": "extension/refresh-build",
295
287
  "entry": "adapters/extension/refresh-build.sh",
@@ -506,22 +498,6 @@
506
498
  "inputs": "--target <checkout>",
507
499
  "outputs": "corepack yarn install output; exit 0/1/2"
508
500
  },
509
- {
510
- "id": "lib/browser-resolver",
511
- "entry": "adapters/shared/browser-resolver.cjs",
512
- "kind": "node",
513
- "purpose": "Choose the extension browser (RECIPE_HARNESS_BROWSER=auto|cft|chrome|<path>; probe-launch Chrome for Testing, cached per machine and version, branded Chrome fallback) and load an unpacked extension over CDP for branded Chrome.",
514
- "inputs": "resolve --target <checkout> [--launch-method spawn|launch-services] [--display headless|headful] [--recorded <file>] [--out <file>] [--json] | load-unpacked --port <cdp> --path <dist> --profile <dir> [--expect-id <id>] [--open-url <url>]; env RECIPE_HARNESS_BROWSER, RECIPE_HARNESS_CHROME_BIN, RECIPE_HARNESS_CHROME_CANDIDATES, RECIPE_HARNESS_BROWSER_CACHE",
515
- "outputs": "executable path or resolution JSON on stdout; probe cache under ~/.cache/mm-harness; exit 0/1"
516
- },
517
- {
518
- "id": "lib/browser-cdp",
519
- "entry": "adapters/shared/browser-cdp.cjs",
520
- "kind": "lib",
521
- "purpose": "Browser-level CDP client with per-command deadlines; prove a CDP port belongs to a slot profile, load an unpacked extension (Extensions.loadUnpacked), audit for other user extensions.",
522
- "inputs": "CDP port, extension dist, owning profile",
523
- "outputs": "loaded extension id or an error; never loads into a browser it does not own"
524
- },
525
501
  {
526
502
  "id": "lib/resolve-slot-ports",
527
503
  "entry": "adapters/shared/resolve-slot-ports.mjs",
@@ -7,11 +7,13 @@
7
7
  // what makes the runner the single source of truth: edit a runner adapter file and
8
8
  // the next launch refreshes every slot from it.
9
9
  //
10
- // stat-only (size + mtime) so it is cheap enough to run on every launch; no file
11
- // contents are read and no Date.now()/random is used (both would break determinism).
10
+ // Source files count by size + mtime, so it is cheap enough to run on every launch;
11
+ // the only file read is @farmslot/adapter-web's package.json, for its version. No
12
+ // Date.now()/random is used (both would break determinism).
12
13
  import fs from 'node:fs';
13
14
  import path from 'node:path';
14
15
  import crypto from 'node:crypto';
16
+ import { createRequire } from 'node:module';
15
17
  import { pathToFileURL } from 'node:url';
16
18
 
17
19
  export function harnessSourceFingerprint(runnerDir, adapter = 'extension') {
@@ -23,6 +25,18 @@ export function harnessSourceFingerprint(runnerDir, adapter = 'extension') {
23
25
  for (const rel of sourceDirs) {
24
26
  collect(path.join(runnerDir, rel), runnerDir, entries);
25
27
  }
28
+ // inject.mjs copies the browser helpers from @farmslot/adapter-web, so its sources
29
+ // are overlay inputs too; label them by package so the install location does not count.
30
+ if (adapter === 'extension') {
31
+ const packageJson = createRequire(path.join(runnerDir, 'package.json')).resolve('@farmslot/adapter-web/package.json');
32
+ const packageRoot = path.dirname(packageJson);
33
+ const packageEntries = [];
34
+ collect(path.join(packageRoot, 'src'), packageRoot, packageEntries);
35
+ entries.push(...packageEntries.map((entry) => `@farmslot/adapter-web/${entry}`));
36
+ // npm tarballs carry fixed mtimes, so an upgrade can keep every size and mtime:
37
+ // the version marks it.
38
+ entries.push(`@farmslot/adapter-web@${JSON.parse(fs.readFileSync(packageJson, 'utf8')).version}`);
39
+ }
26
40
  entries.sort();
27
41
  return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
28
42
  }
@@ -46,7 +46,7 @@ import { hostResolverRules, venueHosts } from './lib/mainnet-guard.mjs';
46
46
  import { assertLaunchNetwork, devServerTestnetDiagnostic } from './lib/readiness.mjs';
47
47
 
48
48
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
49
- const macosFocus = createRequire(import.meta.url)('../extension/lib/macos-focus.cjs');
49
+ const macosFocus = createRequire(import.meta.url)('@farmslot/adapter-web/macos-focus');
50
50
  const HARNESS_ROOT = path.resolve(SCRIPT_DIR, '../..');
51
51
  const FIXTURE_STATE_SCRIPT = path.join(HARNESS_ROOT, 'adapters/extension/wallet-fixture-state.cjs');
52
52
 
@@ -217,7 +217,7 @@ const FOCUS_OUTCOMES = {
217
217
  off: () => 'focus handling is off, or no app was captured before the launch',
218
218
  };
219
219
 
220
- // The one focus check after a launch (shared macos-focus.cjs): the app in
220
+ // The one focus check after a launch (@farmslot/adapter-web/macos-focus): the app in
221
221
  // front before the launch gets the front back, once, by its pid, only if one
222
222
  // of this slot's browser processes (by owner marker, never by bundle) is in
223
223
  // front.
@@ -36,7 +36,7 @@ import { isAppTopFrameContext, isAppUrl as isExactAppUrl } from './lib/origin.mj
36
36
  import { LOG_BINDING, REQUEST_BINDING, RESOLVE_FN, pageScriptSource } from './lib/page-script.mjs';
37
37
  import { createStrictWallet } from './lib/strict-wallet.mjs';
38
38
 
39
- const { captureMacFrontmost, macFocusDisabled } = createRequire(import.meta.url)('../extension/lib/macos-focus.cjs');
39
+ const { captureMacFrontmost, macFocusDisabled } = createRequire(import.meta.url)('@farmslot/adapter-web/macos-focus');
40
40
 
41
41
  function usage() {
42
42
  return 'Usage: wallet-host.mjs --cdp-port <port> --runtime-dir <dir> --signer <extension|injected> --app-origin <url> [--network testnet|mainnet] [--mainnet-hosts <h1,h2>] [--testnet-hosts <h1,h2>] [--extension-id <id>] [--target <checkout> --account <name> --start-chain <id>] [--observe-focus 1] [--window x,y,w,h] [--start-path <path>] [--mm-harness-owner=<marker>]';
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import path from "node:path";
6
- import { recipeRuntimeDir, runnerDir } from "../../paths.js";
6
+ import { recipeRuntimeDir } from "../../paths.js";
7
7
  function extensionBrowserDoctorCheck(target) {
8
8
  const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
9
9
  for (const packageName of ["@playwright/test", "playwright"]) {
@@ -31,7 +31,7 @@ function extensionBrowserDoctorCheck(target) {
31
31
  };
32
32
  }
33
33
  function extensionBrowserResolutionDoctorCheck(target) {
34
- const resolver = path.join(runnerDir, "adapters/shared/browser-resolver.cjs");
34
+ const resolver = createRequire(import.meta.url).resolve("@farmslot/adapter-web/browser-resolver");
35
35
  const runtime = path.join(path.resolve(target), recipeRuntimeDir());
36
36
  const record = path.join(runtime, "browser-resolution.json");
37
37
  let profile = process.env.CHROME_USER_DATA_DIR || path.join(runtime, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
@@ -11,7 +11,7 @@ function readRecord(file) {
11
11
  return null;
12
12
  }
13
13
  }
14
- const browserCdp = createRequire(import.meta.url)("../../../adapters/shared/browser-cdp.cjs");
14
+ const browserCdp = createRequire(import.meta.url)("@farmslot/adapter-web/browser-cdp");
15
15
  function verifyLaunch(record, cdpPort) {
16
16
  const launch = isRecord(record.launch) ? record.launch : {};
17
17
  const pid = Number(launch.pid);
@@ -6,8 +6,8 @@ import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeC
6
6
  import { poolFilesForHost } from "../resolve-slot-ports.js";
7
7
  import { profileProcessPids } from "./validation-process-ownership.js";
8
8
  const require2 = createRequire(import.meta.url);
9
- const { detachedLaunchUnprovenPath, hasDetachedLaunchUnproven, shellQuote } = require2(
10
- "../../../adapters/extension/lib/chrome-args.cjs"
9
+ const { RUNTIME_NONCE_PREFIX, detachedLaunchUnprovenPath, hasDetachedLaunchUnproven, shellQuote } = require2(
10
+ "@farmslot/adapter-web/chrome-args"
11
11
  );
12
12
  const {
13
13
  CdpSession,
@@ -697,7 +697,7 @@ ${result.stderr}`);
697
697
  };
698
698
  }
699
699
  async function startValidationLaunchSupervisor(cdpPort) {
700
- const supervisorPath = path.join(runnerDir, "adapters/extension/lib/validation-launch-supervisor.cjs");
700
+ const supervisorPath = require2.resolve("@farmslot/adapter-web/validation-launch-supervisor");
701
701
  const child = spawn(process.execPath, [supervisorPath, String(cdpPort)], {
702
702
  cwd: runnerDir,
703
703
  env: process.env,
@@ -972,7 +972,7 @@ async function browserEchoesRuntimeNonce(cdpPort, nonce) {
972
972
  2e3
973
973
  );
974
974
  if (!Array.isArray(result.arguments)) return false;
975
- const expected = `--mm-harness-runtime-nonce=${nonce}`;
975
+ const expected = `${RUNTIME_NONCE_PREFIX}${nonce}`;
976
976
  return result.arguments.filter((argument) => argument === expected).length === 1;
977
977
  } catch {
978
978
  return false;
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  const require2 = createRequire(import.meta.url);
3
3
  const processOwnership = require2(
4
- "../../../adapters/extension/lib/validation-process-ownership.cjs"
4
+ "@farmslot/adapter-web/validation-process-ownership"
5
5
  );
6
6
  const { profileProcessPids, stopProfileProcesses, stopProfileProcessesSync } = processOwnership;
7
7
  export {
@@ -34,7 +34,7 @@ import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags
34
34
  import { sameFeatureFlags } from "../../feature-flags.js";
35
35
  const { CdpSession } = await importRecipeHarnessRuntimeCdp();
36
36
  const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
37
- path.join(runnerDir, "adapters/extension/lib/chrome-args.cjs")
37
+ "@farmslot/adapter-web/chrome-args"
38
38
  );
39
39
  function extensionDepsBlock(target) {
40
40
  if (!fs.existsSync(path.join(target, "package.json"))) return null;
@@ -90,22 +90,11 @@ These are real farm bugs. The goldens freeze the current rejections, so fixing e
90
90
  | Mobile templates `dev.md`, `dev-interactive.md`, `fix-bug.md`, `review-pr.md` | `mark complete --status blocked --outcome partial --reason …` exits 2 | `mark blocked --reason "<why>"` | `task/mark-invalid-status-outcome`, `task/mark-blocked` |
91
91
  | Docs | `launch --build-lavamoat` (only `runtime-launch` has it); `run --target <recipe-file>` | `runtime-launch --build-lavamoat`; `run <recipe-file> --target <repo>` | — |
92
92
 
93
- ## Terminal adapter: TODO
94
-
95
- #298 merged the `terminal` adapter (`fd1c77e`). Discovery already reflects it (`discovery/actions-matrix`, the adapter hint in `help`). **Follow-up, not in this PR:** freeze its farm hooks once a stub browser replaces the LaunchServices launch of a visible Chrome for Testing (the goldens must never start a real browser). Add `tests/contract/goldens-terminal.test.sh` covering the `va-mmcx-terminal-farm` hooks:
96
-
97
- - [ ] `run <recipe> --adapter terminal --artifacts-dir <d> --target <repo> --slot <s> --cdp-port <p> --watcher-port <p> --json`
98
- - [ ] `actions --raw --adapter terminal --json`
99
- - [ ] `doctor --adapter terminal --target <repo> --cdp-port <p> --watcher-port <p> --json`
100
- - [ ] `install --adapter terminal --target <repo>`
101
- - [ ] `verify --adapter terminal --target <repo> -- --cdp-port <p> --watcher-port <p> --account <a>`
102
- - [ ] `cleanup --adapter terminal --target <repo> -- --cdp-port <p>`
103
- - [ ] `launch --adapter terminal --target <repo> --cdp-port <p> --watcher-port <p> --signer extension --account <a>` (use the stub browser from #298's `tests/fixtures/terminal-stub-browser.mjs`)
104
- - [ ] `stop --adapter terminal --target <repo>` with `RECIPE_CDP_PORT`
105
- - [ ] `run terminal.perps.<name> k=v… --adapter terminal --target <t> --cdp-port <p> --watcher-port <p> --plan` (recipe-terminal `checks/plan-recipes.mjs`)
106
- - [ ] runtime layout of `temp/recipe/runtime/terminal/` (`browser.pid` is watched by the farm)
107
- - [ ] discovery: add `terminal` to the per-adapter loop in `goldens-discovery.test.sh` (`actions --raw`, `actions`, `run --list`, `call --list`)
108
- - [ ] recipe-terminal `console-guard.mjs` imports the harness's `library/actions/shared/console-findings.mjs` and `tests/fixtures/console-allowlist-vectors.json`: freeze both paths and the exported surface
93
+ ## Terminal adapter
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.
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.
109
98
 
110
99
  ## Running the goldens
111
100
 
@@ -152,7 +141,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
152
141
  1. Absolute paths: the sandbox becomes `<SANDBOX>`, the repo `<REPO>`, the real `$HOME` `<HOME>`, the node binary and its prefix `<NODE>`/`<NODE_PREFIX>`, and `os.tmpdir()` `<TMP>`. Both `/var` and `/private/var` spellings are covered. Ports from `gd_reserve_ports` become `<PORT:NAME>` in port positions only: numbers under `*port` keys, and in text after `:`, `port `, `port=` or `PORT=`, or a string that is exactly the port (`^<port>$`; `60000 ms` is left alone).
153
142
  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>`.
154
143
  3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
155
- 4. Numbers under volatile keys: `*pid` becomes `<PID>`, `*port` `<PORT>`, `*At`/`*Time`/`*timestamp`/`mtime*` `<TIMESTAMP>`, and `*Ms`/`duration*`/`elapsed*` `<DURATION>`. Values inside `schema` and `examples` subtrees (static manifest content) are never rewritten.
144
+ 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.
156
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.
157
146
  6. Durations in text (`0.3s`, `123ms`) become `<DURATION>`.
158
147
 
@@ -19,7 +19,6 @@ import { closeSync, openSync } from 'node:fs';
19
19
  import { spawn } from 'node:child_process';
20
20
  import { createRequire } from 'node:module';
21
21
  import path from 'node:path';
22
- import { fileURLToPath } from 'node:url';
23
22
 
24
23
  import {
25
24
  importRecipeHarnessRuntimeBrowserExtension,
@@ -370,11 +369,10 @@ function autolaunchEnabled(input) {
370
369
  }
371
370
 
372
371
  const runnerRequire = createRequire(import.meta.url);
373
- const runnerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
374
- const browserResolver = runnerRequire(path.join(runnerRoot, 'adapters/shared/browser-resolver.cjs'));
375
- const { stopProfileProcessesSync } = runnerRequire(path.join(runnerRoot, 'adapters/extension/lib/validation-process-ownership.cjs'));
376
- const browserCdp = runnerRequire(path.join(runnerRoot, 'adapters/shared/browser-cdp.cjs'));
377
- const macosFocus = runnerRequire(path.join(runnerRoot, 'adapters/extension/lib/macos-focus.cjs'));
372
+ const browserResolver = runnerRequire('@farmslot/adapter-web/browser-resolver');
373
+ const { stopProfileProcessesSync } = runnerRequire('@farmslot/adapter-web/validation-process-ownership');
374
+ const browserCdp = runnerRequire('@farmslot/adapter-web/browser-cdp');
375
+ const macosFocus = runnerRequire('@farmslot/adapter-web/macos-focus');
378
376
 
379
377
  // On macOS a browser that opens a window at startup activates itself, so the
380
378
  // autolaunched browser starts without one and gets its pages over CDP in the
@@ -382,7 +380,7 @@ const macosFocus = runnerRequire(path.join(runnerRoot, 'adapters/extension/lib/m
382
380
  const startsWithoutWindow = () => process.platform === 'darwin';
383
381
 
384
382
  // One bounded browser-level CDP operation (the shared helpers in
385
- // browser-cdp.cjs never take the operator's focus).
383
+ // @farmslot/adapter-web/browser-cdp never take the operator's focus).
386
384
  async function withBrowserCdp(port, fn) {
387
385
  const deadline = Date.now() + 15000;
388
386
  const remaining = () => Math.max(1, deadline - Date.now());
@@ -1,7 +1,7 @@
1
1
  // Select the CDP page a terminal 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
- // MM_HARNESS_FOCUS_BROWSER=1, as in the Extension adapter: Page.bringToFront
4
+ // FARMSLOT_FOCUS_BROWSER=1, as in the Extension adapter: Page.bringToFront
5
5
  // activates the browser on macOS and takes the operator's keyboard focus. The
6
6
  // app tab is the only tab of its window (the wallet host closes the others),
7
7
  // so it stays the window's active tab and keeps painting without it.
@@ -199,7 +199,7 @@ export function slowMoFor(browserState, env = process.env) {
199
199
  }
200
200
 
201
201
  export function focusBrowserRequested(env = process.env) {
202
- return env.MM_HARNESS_FOCUS_BROWSER === '1';
202
+ return env.FARMSLOT_FOCUS_BROWSER === '1';
203
203
  }
204
204
 
205
205
  // Defence in depth before a wallet confirmation: the app port is still served
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,6 +21,7 @@
21
21
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
22
22
  },
23
23
  "dependencies": {
24
+ "@farmslot/adapter-web": "0.1.0",
24
25
  "@farmslot/agent-runtime": "0.16.0",
25
26
  "@farmslot/expo-recipe": "0.13.1",
26
27
  "@farmslot/handoff": "^0.3.1",