@deeeed/metamask-harness 0.72.0 → 0.73.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.73.0 - 2026-10-03
6
+
7
+ ### Changed
8
+
9
+ - Recipe discovery runs on `@farmslot/recipe-cli`: `run --list` and recipe completion candidates use its readiness rule, and `run --describe` and recipe assistance use its call-graph composition. mm-harness still supplies the view and the policy: only the requested adapter's libraries are loaded, and readiness is judged against the MetaMask manifest `run` executes, so a broken recipe for another platform no longer matters and team manifests follow the same rules as `run --plan`. Flags and output are unchanged; the discovery goldens prove it.
10
+ - Move to the Farmslot release that ships `@farmslot/recipe-cli`: `@farmslot/protocol` 0.34.0, `@farmslot/recipe-harness` 0.22.1, `@farmslot/recipe-cli` 0.1.1, `@farmslot/agent-runtime` 0.16.0 and `@farmslot/expo-recipe` 0.13.1, all on one protocol and one recipe-harness copy. With recipe-harness 0.22, a library's `recipes/terminal/` folder counts as the terminal variant of its plain ids, so `run --list --adapter terminal` with the terminal library also lists `perps.manage-position` and its siblings; the `terminal.perps.*` ids are unchanged.
11
+
5
12
  ## 0.72.0 - 2026-10-03
6
13
 
7
14
  ### Added
@@ -30,6 +30,8 @@ import {
30
30
  validateMetaMaskActionInputs
31
31
  } from "../metamask-action-validation.js";
32
32
  import { gitLibraryProvenance } from "../library-provenance.js";
33
+ import { runnableLibraryRecipes } from "../discovery.js";
34
+ import { recipeComposition } from "@farmslot/recipe-cli";
33
35
  import {
34
36
  captureExecutionProvenance,
35
37
  executionProvenanceDrift,
@@ -710,35 +712,7 @@ function effectiveLibrarySources(librarySources) {
710
712
  return librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: path.resolve(runnerDir, "library") }];
711
713
  }
712
714
  async function listRunnableRecipes(adapter, librarySources) {
713
- const harness = await importRecipeHarness();
714
- const sources = effectiveLibrarySources(librarySources);
715
- const resolution = await harness.loadRecipeLibraries(
716
- sources,
717
- {
718
- adapter
719
- }
720
- );
721
- const protocol = await importRecipeProtocol();
722
- const { manifest } = await resolveActionManifest(adapter, void 0, sources);
723
- const externalRecipeIds = new Set(resolution.recipes.keys());
724
- const validRefs = new Set([...resolution.recipes.values()].filter((recipe) => protocol.validateRecipeWithManifest(recipe.document, manifest, {
725
- externalRecipeIds
726
- }).status === "valid").map((recipe) => recipe.ref));
727
- return [...resolution.recipes.values()].filter((recipe) => {
728
- if (!validRefs.has(recipe.ref)) return false;
729
- try {
730
- const dependencies = harness.resolveRecipeDependencies({
731
- rootRef: recipe.ref,
732
- root: recipe.document,
733
- rootSource: recipe.provenance,
734
- recipes: resolution.recipes
735
- });
736
- return [...dependencies.recipes.keys()].every((ref) => validRefs.has(ref));
737
- } catch (error) {
738
- if (error instanceof harness.RecipeResolutionError) return false;
739
- throw error;
740
- }
741
- }).map(
715
+ return (await runnableLibraryRecipes(adapter, effectiveLibrarySources(librarySources))).map(
742
716
  (recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
743
717
  ).sort((left, right) => left.name.localeCompare(right.name));
744
718
  }
@@ -822,42 +796,6 @@ function recipeParameters(document) {
822
796
  };
823
797
  });
824
798
  }
825
- function recipeComposition(recipe, recipes) {
826
- const actions = /* @__PURE__ */ new Set();
827
- const nestedRecipes = /* @__PURE__ */ new Set();
828
- const unresolvedRecipes = /* @__PURE__ */ new Set();
829
- const visitedRecipes = /* @__PURE__ */ new Set();
830
- const visitNode = (value) => {
831
- if (!isRecord(value) || typeof value.action !== "string") return;
832
- if (value.action !== "call") {
833
- actions.add(value.action);
834
- return;
835
- }
836
- if (typeof value.ref !== "string") return;
837
- nestedRecipes.add(value.ref);
838
- const dependency = recipes.get(value.ref);
839
- if (!dependency) {
840
- unresolvedRecipes.add(value.ref);
841
- return;
842
- }
843
- if (visitedRecipes.has(value.ref)) return;
844
- visitedRecipes.add(value.ref);
845
- visitRecipe(dependency.document);
846
- };
847
- const visitWorkflow = (value) => {
848
- if (!isRecord(value)) return;
849
- if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
850
- };
851
- const visitRecipe = (document) => {
852
- visitWorkflow(document.workflow);
853
- };
854
- visitRecipe(recipe);
855
- return {
856
- actions: [...actions].sort(),
857
- nestedRecipes: [...nestedRecipes].sort(),
858
- unresolvedRecipes: [...unresolvedRecipes].sort()
859
- };
860
- }
861
799
  async function validateRunRecipeStatic(recipeArg, adapter, options, params = {}) {
862
800
  let librarySources = await resolveMetaMaskLibrarySources(
863
801
  optionStrings(options, "library")
@@ -1492,7 +1430,6 @@ export {
1492
1430
  preflightRecipe,
1493
1431
  prepareHeal,
1494
1432
  prepareRuntimeIfNeeded,
1495
- recipeComposition,
1496
1433
  resolveMetaMaskLibrarySources,
1497
1434
  resolveRunRecipeArg,
1498
1435
  runOneNode,
@@ -0,0 +1,15 @@
1
+ import { assessRecipe } from "@farmslot/recipe-cli";
2
+ import {
3
+ loadRecipeLibraries
4
+ } from "@farmslot/recipe-harness";
5
+ import { resolveActionManifest } from "./manifest.js";
6
+ async function runnableLibraryRecipes(adapter, sources) {
7
+ const resolution = await loadRecipeLibraries(sources, { adapter });
8
+ const { manifest } = await resolveActionManifest(adapter, void 0, sources);
9
+ return [...resolution.recipes.values()].filter(
10
+ (recipe) => assessRecipe({ resolution, manifest }, recipe).length === 0
11
+ );
12
+ }
13
+ export {
14
+ runnableLibraryRecipes
15
+ };
@@ -4,7 +4,8 @@ import { loadRecipeLibraries, resolveRecipeDependencies, rootResolutionRef } fro
4
4
  import { isSensitiveKey } from "./command-journal.js";
5
5
  import { resolveActionManifest } from "./manifest.js";
6
6
  import { describeManifestActions } from "./commands/manifest.js";
7
- import { recipeComposition, resolveMetaMaskLibrarySources, validateRunRecipeStatic } from "./commands/run-engine.js";
7
+ import { recipeComposition } from "@farmslot/recipe-cli";
8
+ import { resolveMetaMaskLibrarySources, validateRunRecipeStatic } from "./commands/run-engine.js";
8
9
  import { CliError, isRecord, optionStrings } from "./commands/parse-args.js";
9
10
  import { EXIT } from "./commands/shared.js";
10
11
  import { MAX_ADVICE_STATE_BYTES } from "./recipe-assistance-rubric.js";
@@ -12,6 +12,7 @@ bin/mm-harness
12
12
  -> src/ typed CLI and product decisions
13
13
  -> @farmslot/agent-runtime checklist state
14
14
  -> @farmslot/handoff scrubbed learning capture/share
15
+ -> @farmslot/recipe-cli recipe discovery: libraries, runnable checks, composition
15
16
  -> @farmslot/recipe-harness generic execution, UI transports, evidence
16
17
  -> @farmslot/protocol schemas
17
18
  -> adapters/ focused host/browser/device leaves
@@ -24,6 +25,7 @@ bin/mm-harness
24
25
  | `@farmslot/protocol` | recipe and evidence schemas |
25
26
  | `@farmslot/agent-runtime` | task-local checklist state and terminal contracts |
26
27
  | `@farmslot/handoff` | learning assembly, scrubbing, validation, approval, and publication |
28
+ | `@farmslot/recipe-cli` | recipe discovery: library resolution, runnable checks, composition |
27
29
  | `@farmslot/recipe-harness` | generic execution, recovery, traces, artifacts, `ui.*` |
28
30
  | `mm-harness` | MetaMask runtime control, diagnostics, durable domain actions |
29
31
  | skills/checklists | task workflow and proof expectations |
@@ -122,10 +122,10 @@ Each case runs under a safety cap, `GOLDENS_CASE_TIMEOUT` (default 600 s). A cas
122
122
  Each family is a normal contract test (`tests/contract/goldens-<family>.test.sh`). `gd_init` (`tests/contract/goldens/lib.sh`) builds a sandbox on `ct_init`, and every case runs under `env -i`:
123
123
  - `gd_init` first unsets every inherited `MM_HARNESS_*`, `RECIPE_*`, `METAMASK_*` and `FARMSLOT_*` variable, plus device and port pins (`IOS_SIMULATOR`, `ADB_SERIAL`, `WATCHER_PORT`, `METRO_PORT`, `CDP_PORT`, `IDB_PATH`, `ANDROID_HOME`, …). A developer's `MM_HARNESS_BIN` or `MM_HARNESS_IDB_PATH` export therefore cannot redirect a case;
124
124
  - the environment is an allowlist: sandbox `HOME`, caches, config, `FARMSLOT_HOME`, `TZ=UTC`, no update probe, and only the variables the family sets itself;
125
- - every interpreter and tool the goldens launch is resolved to its real binary before `HOME` moves into the sandbox: `node` (`process.execPath`), `python3` (`sys.executable`, for the test shell's port holder only), `git` (`git --exec-path`), `bash` (`$BASH`) and `timeout`. A tool that resolves to a script, the shape of an asdf/mise/volta/pyenv shim, fails the family up front with a message naming it. Each is checked to run with a sandboxed `HOME`;
125
+ - every interpreter and tool the goldens launch is resolved to its real binary before `HOME` moves into the sandbox: `node` (`process.execPath`), `git` (`git --exec-path`), `bash` (`$BASH`) and `timeout`. The goldens need no Python. A tool that resolves to a script, the shape of an asdf/mise/volta/pyenv shim, fails the family up front with a message naming it. Each is checked to run with a sandboxed `HOME`;
126
126
  - `PATH` is pinned to the case stubs, the sandbox stubs, those real binaries, and `/usr/bin:/bin:/usr/sbin:/sbin`. Host tools (`idb`, `capture-helper`, a developer's Chrome) cannot change a golden;
127
127
  - device-tool discovery is pinned: `MM_HARNESS_IDB_PATH` and `MM_HARNESS_ADB_PATH` point at sandbox stubs, which discovery checks before any absolute Homebrew or pipx path;
128
- - localhost probes cannot reach a real server. `gd_reserve_ports` holds ephemeral ports that are bound but never listened on (`127.0.0.1` and `::1`), so Metro `/status` and CDP probes cannot connect (on macOS they time out rather than being refused) and nothing real can take the port mid-run. Each family exports the farm's slot port variables (`RECIPE_CDP_PORT`, `WATCHER_PORT`, `METRO_PORT`) set to those ports, so commands without an explicit port use them too. Goldens show them as `<PORT:NAME>`, rewritten only in port positions (`*port` keys, `:<port>`, `port <n>`/`PORT=<n>`, or an argument that is exactly the port), so an unrelated equal number such as a 60000 ms timeout is never rewritten;
128
+ - localhost probes cannot reach a real server. `gd_reserve_ports` runs a Node port holder (`tests/contract/goldens/port-holder.mjs`) that holds ephemeral ports on `127.0.0.1` and `::1` and resets every connection it accepts, so Metro `/status` and CDP probes never get a response and nothing real can take the port mid-run. Each family exports the farm's slot port variables (`RECIPE_CDP_PORT`, `WATCHER_PORT`, `METRO_PORT`) set to those ports, so commands without an explicit port use them too. Goldens show them as `<PORT:NAME>`, rewritten only in port positions (`*port` keys, `:<port>`, `port <n>`/`PORT=<n>`, or a string that is exactly the port), so an unrelated equal number such as a 60000 ms timeout is never rewritten;
129
129
  - `tmux`, `xcrun` and `adb` are stubbed (`ct_init`, plus one booted simulator and one Android device for Mobile);
130
130
  - forbidden stubs record any call to `open`, `osascript`, `lsappinfo`, `screencapture`, `yarn`, `npx`, `npm`, `corepack`, `gh` and Chrome/Chromium, and the family fails if one is called. A browser stub answers only `--version`, and `npm` only `root -g` (install-source detection), with a sandbox path;
131
131
  - inert stubs on the case PATH only: `lsof`, `pgrep` and `pkill` find nothing, `ps` passes per-pid queries (`-p`, the CLI identifying its own processes) and lists an empty host for table listings, and `watchman` and `caffeinate` are no-ops. `stop` cannot see or signal a real process or touch a host daemon;
@@ -149,7 +149,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
149
149
 
150
150
  `tests/contract/goldens/normalize.mjs` is the only normaliser. It rewrites only volatile values:
151
151
 
152
- 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 whole string equal to the port.
152
+ 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
153
  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
154
  3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
155
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.72.0",
3
+ "version": "0.73.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/agent-runtime": "0.15.1",
25
- "@farmslot/expo-recipe": "0.12.2",
24
+ "@farmslot/agent-runtime": "0.16.0",
25
+ "@farmslot/expo-recipe": "0.13.1",
26
26
  "@farmslot/handoff": "^0.3.1",
27
- "@farmslot/protocol": "0.33.0",
28
- "@farmslot/recipe-harness": "0.21.1",
27
+ "@farmslot/protocol": "0.34.0",
28
+ "@farmslot/recipe-cli": "0.1.1",
29
+ "@farmslot/recipe-harness": "0.22.1",
29
30
  "agent-device": "0.19.3",
30
31
  "commander": "^12.0.0",
31
32
  "es-module-lexer": "2.3.1",