@deeeed/metamask-harness 0.76.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 (35) hide show
  1. package/CHANGELOG.md +14 -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/call.js +3 -7
  20. package/dist/commands/launch/extension.js +1 -1
  21. package/dist/commands/list-executables.js +12 -22
  22. package/dist/commands/manifest.js +11 -133
  23. package/dist/commands/run.js +2 -5
  24. package/docs/cli-contract.md +6 -17
  25. package/library/actions/extension/platform/cdp.mjs +5 -7
  26. package/library/actions/terminal/platform/page.mjs +2 -2
  27. package/package.json +3 -2
  28. package/adapters/extension/lib/chrome-args.cjs +0 -365
  29. package/adapters/extension/lib/extension-id.cjs +0 -36
  30. package/adapters/extension/lib/macos-focus.cjs +0 -108
  31. package/adapters/extension/lib/playwright-cdp.cjs +0 -50
  32. package/adapters/extension/lib/validation-launch-supervisor.cjs +0 -292
  33. package/adapters/extension/lib/validation-process-ownership.cjs +0 -125
  34. package/adapters/shared/browser-cdp.cjs +0 -542
  35. package/adapters/shared/browser-resolver.cjs +0 -1007
@@ -17,6 +17,15 @@ import {
17
17
  } from "@farmslot/protocol";
18
18
  import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
19
19
  import { closest } from "../command-contract.js";
20
+ import {
21
+ actionCapabilityMatrix,
22
+ actionCategory,
23
+ findRelatedActions,
24
+ fuzzyResolveActions,
25
+ resolveActionCapabilityRefusal,
26
+ searchActions,
27
+ summarizeActionCategories
28
+ } from "@farmslot/recipe-cli";
20
29
  const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
21
30
  const ACTION_MATRIX_ADAPTERS = ["mobile", "extension", "core", "terminal"];
22
31
  function actionLibraryContextArgs(librarySources) {
@@ -178,51 +187,7 @@ async function resolveActionCapabilityMatrix(librarySources) {
178
187
  return { adapter, actions: describeManifestActions(manifest, actionSources) };
179
188
  })
180
189
  );
181
- const actions = /* @__PURE__ */ new Map();
182
- for (const catalog of catalogs) {
183
- for (const action of catalog.actions) {
184
- const entry = actions.get(action.name) ?? { entries: [], adapters: /* @__PURE__ */ new Set() };
185
- entry.entries.push(action);
186
- entry.adapters.add(catalog.adapter);
187
- actions.set(action.name, entry);
188
- }
189
- }
190
- return [...actions.entries()].map(([name, entry]) => {
191
- const representative = entry.entries[0];
192
- const satisfyingAdapters = ACTION_MATRIX_ADAPTERS.filter(
193
- (adapter) => entry.adapters.has(adapter)
194
- );
195
- return {
196
- name,
197
- category: representative.category,
198
- description: entry.entries.map((action) => action.description).find(Boolean) ?? "",
199
- fields: [...new Set(entry.entries.flatMap((action) => action.fields))].sort(),
200
- support: Object.fromEntries(
201
- ACTION_MATRIX_ADAPTERS.map((adapter) => [
202
- adapter,
203
- entry.adapters.has(adapter) ? "available" : "unavailable"
204
- ])
205
- ),
206
- satisfyingAdapters
207
- };
208
- }).sort((left, right) => left.name.localeCompare(right.name));
209
- }
210
- function resolveActionCapabilityRefusal(action, adapter, matrix) {
211
- const matches = fuzzyResolveActions(matrix, action);
212
- if (matches.length !== 1 || matches[0].satisfyingAdapters.includes(adapter)) {
213
- return void 0;
214
- }
215
- return {
216
- capability: matches[0].name,
217
- satisfyingAdapters: matches[0].satisfyingAdapters
218
- };
219
- }
220
- function missingActionCapabilities(adapter, actionNames, matrix) {
221
- return [...new Set(actionNames)].flatMap((action) => {
222
- const row = matrix.find((entry) => entry.name === action);
223
- if (!row || row.satisfyingAdapters.includes(adapter)) return [];
224
- return [{ capability: row.name, satisfyingAdapters: row.satisfyingAdapters }];
225
- });
190
+ return actionCapabilityMatrix(catalogs);
226
191
  }
227
192
  function handleActionMatrixError(json, code, message, userAction, details = {}) {
228
193
  if (json) {
@@ -315,32 +280,6 @@ function renderHumanActionMatrix(actions) {
315
280
  )
316
281
  ].join("\n");
317
282
  }
318
- function fuzzyResolveActions(entries, query) {
319
- const exactFull = entries.filter((e) => e.name === query);
320
- if (exactFull.length > 0) return exactFull;
321
- const finalSegment2 = (name) => name.split(".").pop() ?? name;
322
- const exactSegment = entries.filter((e) => finalSegment2(e.name) === query);
323
- if (exactSegment.length > 0) return exactSegment;
324
- return entries.filter((e) => finalSegment2(e.name).includes(query));
325
- }
326
- function searchActions(entries, query) {
327
- const terms = searchTerms(query);
328
- if (terms.length === 0) return [];
329
- return entries.map((entry) => {
330
- const scores = terms.map((term) => actionSearchScore(entry, term));
331
- return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
332
- }).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
333
- }
334
- function findRelatedActions(entries, selected, limit = 5) {
335
- const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
336
- return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
337
- const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
338
- const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
339
- const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
340
- return { name: entry.name, score };
341
- }).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
342
- }
343
- const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
344
283
  function summarizeDescribedAction(entry) {
345
284
  const { schema: _schema, examples: _examples, ...summary } = entry;
346
285
  return summary;
@@ -496,11 +435,6 @@ function actionDomainGroup(entry) {
496
435
  const match = /^metamask[.]([^.]+)[.]/u.exec(entry.name);
497
436
  return match ? `metamask \xB7 ${match[1]}` : "custom";
498
437
  }
499
- function summarizeActionCategories(actions) {
500
- const counts = /* @__PURE__ */ new Map();
501
- for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
502
- return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
503
- }
504
438
  function describeManifestActions(manifest, actionSources = /* @__PURE__ */ new Map()) {
505
439
  const manifestRecord = isRecord(manifest) ? manifest : {};
506
440
  const actions = isRecord(manifestRecord.actions) ? manifestRecord.actions : {};
@@ -521,7 +455,7 @@ function describeManifestAction(name, kind, metadata, source) {
521
455
  return {
522
456
  name,
523
457
  kind,
524
- category: actionCategory(name),
458
+ category: actionCategory(name, ["metamask"]),
525
459
  description: typeof record.description === "string" ? record.description : "",
526
460
  fields: properties,
527
461
  schema,
@@ -539,72 +473,16 @@ function describeManifestAction(name, kind, metadata, source) {
539
473
  ])]
540
474
  };
541
475
  }
542
- function actionCategory(name) {
543
- const segments = name.split(".");
544
- if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
545
- if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
546
- if (segments[0] === "ui") return "ui";
547
- if (name.startsWith("assert_")) return "assertion";
548
- if (name === "watch_logs" || name === "index_artifacts") return "evidence";
549
- if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
550
- return segments.length > 1 ? segments[0] || "utility" : "utility";
551
- }
552
- function actionSearchScore(entry, term) {
553
- const name = entry.name.toLowerCase();
554
- const segment = finalSegment(name);
555
- const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
556
- const fields = entry.fields.map((field) => field.toLowerCase());
557
- const description = entry.description.toLowerCase();
558
- if (name === term) return 120;
559
- if (segment === term) return 110;
560
- if (entry.category === term) return 100;
561
- if (fields.includes(term)) return 90;
562
- if (nameTerms.includes(term)) return 80;
563
- if (name.includes(term)) return 70;
564
- if (fields.some((field) => field.includes(term))) return 60;
565
- if (description.includes(term)) return 50;
566
- if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
567
- return 0;
568
- }
569
- function fuzzyTermMatch(term, candidate) {
570
- if (term.length < 4 || candidate.length < 4) return false;
571
- const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
572
- return levenshtein(term, candidate) <= threshold;
573
- }
574
- function searchTerms(value) {
575
- return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
576
- }
577
- function finalSegment(name) {
578
- return name.split(".").pop() ?? name;
579
- }
580
- function levenshtein(left, right) {
581
- const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
582
- for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
583
- const current = [leftIndex];
584
- for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
585
- const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
586
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
587
- }
588
- for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
589
- }
590
- return prior[right.length] ?? right.length;
591
- }
592
476
  export {
593
477
  ACTION_MATRIX_ADAPTERS,
594
478
  actionDomain,
595
479
  actionExampleCommand,
596
480
  actionLibraryContextArgs,
597
481
  describeManifestActions,
598
- findRelatedActions,
599
- fuzzyResolveActions,
600
482
  handleActions,
601
- missingActionCapabilities,
602
483
  renderHumanActionCatalog,
603
484
  renderHumanActionExample,
604
485
  renderHumanActionMatrix,
605
486
  resolveActionCapabilityMatrix,
606
- resolveActionCapabilityRefusal,
607
- searchActions,
608
- summarizeActionCategories,
609
487
  summarizeDescribedAction
610
488
  };
@@ -38,11 +38,8 @@ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run
38
38
  import { recipeTrustFailure } from "../recipe-security.js";
39
39
  import { ProvenanceDriftError } from "../execution-provenance.js";
40
40
  import { recordCommandEvidence } from "../command-journal.js";
41
- import {
42
- actionLibraryContextArgs,
43
- missingActionCapabilities,
44
- resolveActionCapabilityMatrix
45
- } from "./manifest.js";
41
+ import { actionLibraryContextArgs, resolveActionCapabilityMatrix } from "./manifest.js";
42
+ import { missingActionCapabilities } from "@farmslot/recipe-cli";
46
43
  import {
47
44
  startRunNetworkObservation
48
45
  } from "../network-observation.js";
@@ -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.76.0",
3
+ "version": "0.78.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,11 +21,12 @@
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",
27
28
  "@farmslot/protocol": "0.34.0",
28
- "@farmslot/recipe-cli": "0.1.1",
29
+ "@farmslot/recipe-cli": "0.1.2",
29
30
  "@farmslot/recipe-harness": "0.22.1",
30
31
  "agent-device": "0.19.3",
31
32
  "commander": "^12.0.0",