@deeeed/metamask-harness 0.74.0 → 0.76.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 (96) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/adapters/shared/reap-checkout-metros.sh +9 -0
  5. package/dist/action-values.js +15 -0
  6. package/dist/adapters/core/actions.js +20 -0
  7. package/dist/adapters/core/live-script-env.js +36 -0
  8. package/dist/adapters/core/overlay.js +34 -0
  9. package/dist/adapters/core/readiness.js +8 -0
  10. package/dist/adapters/core/run.js +54 -0
  11. package/dist/adapters/core/surface.js +12 -1
  12. package/dist/adapters/extension/action-inputs.js +206 -0
  13. package/dist/adapters/extension/actions.js +51 -0
  14. package/dist/adapters/extension/console-capture.js +13 -1
  15. package/dist/adapters/extension/doctor-checks.js +135 -0
  16. package/dist/adapters/extension/fixtures.js +103 -0
  17. package/dist/adapters/extension/launch.js +315 -0
  18. package/dist/adapters/extension/launched-browser.js +51 -0
  19. package/dist/adapters/extension/overlay.js +55 -0
  20. package/dist/adapters/extension/readiness.js +32 -0
  21. package/dist/adapters/extension/reload.js +40 -0
  22. package/dist/adapters/extension/run.js +105 -0
  23. package/dist/adapters/extension/surface.js +31 -3
  24. package/dist/adapters/harness-freshness.js +4 -2
  25. package/dist/adapters/mobile/actions.js +54 -0
  26. package/dist/adapters/mobile/devices.js +21 -0
  27. package/dist/adapters/mobile/doctor-checks.js +40 -0
  28. package/dist/adapters/mobile/fixtures.js +141 -0
  29. package/dist/adapters/mobile/launch.js +192 -0
  30. package/dist/adapters/mobile/lifecycle.js +300 -0
  31. package/dist/adapters/mobile/overlay.js +27 -0
  32. package/dist/adapters/mobile/prepare-target.js +38 -0
  33. package/dist/adapters/mobile/readiness.js +39 -0
  34. package/dist/adapters/mobile/reload.js +15 -0
  35. package/dist/adapters/mobile/run.js +156 -0
  36. package/dist/adapters/mobile/runtime-identity.js +34 -0
  37. package/dist/adapters/mobile/surface.js +39 -3
  38. package/dist/adapters/mobile/ui.js +701 -0
  39. package/dist/adapters/performance/cdp-trace.js +24 -6
  40. package/dist/adapters/performance/device-platform.js +10 -0
  41. package/dist/adapters/registry.js +1 -1
  42. package/dist/adapters/terminal/actions.js +43 -0
  43. package/dist/adapters/terminal/console-capture.js +14 -1
  44. package/dist/adapters/terminal/doctor-checks.js +22 -0
  45. package/dist/adapters/terminal/overlay.js +18 -0
  46. package/dist/adapters/terminal/readiness.js +11 -0
  47. package/dist/adapters/terminal/run.js +30 -0
  48. package/dist/adapters/terminal/surface.js +21 -3
  49. package/dist/adapters.js +32 -824
  50. package/dist/cli-commands.js +1 -0
  51. package/dist/cli.js +3 -1
  52. package/dist/command-contract.js +3 -0
  53. package/dist/commands/call.js +2 -5
  54. package/dist/commands/debug.js +6 -25
  55. package/dist/commands/device-target.js +2 -1
  56. package/dist/commands/doctor.js +26 -137
  57. package/dist/commands/fixtures.js +13 -217
  58. package/dist/commands/launch/device.js +2 -6
  59. package/dist/commands/launch/extension.js +3 -303
  60. package/dist/commands/launch/index.js +1 -1
  61. package/dist/commands/launch/mobile.js +2 -190
  62. package/dist/commands/launch/quote.js +7 -0
  63. package/dist/commands/logs.js +1 -1
  64. package/dist/commands/parse-args.js +9 -4
  65. package/dist/commands/pin.js +226 -0
  66. package/dist/commands/prepare.js +23 -53
  67. package/dist/commands/provision.js +3 -7
  68. package/dist/commands/reload.js +8 -47
  69. package/dist/commands/run-engine.js +38 -138
  70. package/dist/commands/run-report.js +4 -48
  71. package/dist/commands/run.js +6 -48
  72. package/dist/commands/runtime-decision.js +5 -27
  73. package/dist/commands/self-test.js +1 -3
  74. package/dist/commands/status.js +20 -28
  75. package/dist/commands/stop.js +1 -1
  76. package/dist/doctor.js +12 -120
  77. package/dist/execution-provenance.js +3 -2
  78. package/dist/harness-pin.js +10 -0
  79. package/dist/harness.js +31 -100
  80. package/dist/heal-bounds.js +3 -2
  81. package/dist/live-adapter-contract.js +8 -34
  82. package/dist/manifest.js +3 -5
  83. package/dist/metamask-action-validation.js +5 -196
  84. package/dist/mm-harness-cli.js +30 -4
  85. package/dist/network-observation.js +10 -3
  86. package/dist/paths.js +8 -2
  87. package/dist/performance-observation.js +15 -22
  88. package/dist/pr-body/render.js +3 -1
  89. package/dist/recording-target.js +4 -39
  90. package/dist/run-diagnostics.js +21 -60
  91. package/dist/run-recording.js +5 -4
  92. package/dist/runner.js +20 -368
  93. package/dist/runtime-context.js +12 -17
  94. package/docs/CONTRIBUTING.md +29 -7
  95. package/package.json +2 -1
  96. package/scripts/completions.sh +2 -1
package/CHANGELOG.md CHANGED
@@ -2,11 +2,49 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.76.0 - 2026-10-04
6
+
7
+ ### Changed
8
+
9
+ - The lifecycle and readiness commands reach platform behavior through the adapter surface instead of branching on the adapter name. New `AdapterSurface` members:
10
+ - `harness`: the install/cleanup leaves, verify dispatch and environment, and the restart hint;
11
+ - `runtimeContext`: forbidden fields, local port bases and the resources `agentic-runtime.json` records;
12
+ - `readiness`: doctor's compatibility mode, platform checks, live checks, environment, orphaned dev servers, capture providers, `--fix` repairs, runtime block, pinned feature flags, the status runtime probe, and connected devices;
13
+ - `fixtures` (operations, apply), `prepare`, `debug`, `reload` and `runtimeDecision`;
14
+ - plus `devServer.afterStop` and `runwayProvision.before`/`launchNext`.
15
+ - `harness.ts`, `prepare`, `runtime-context`, `status`, both doctor modules, `fixtures`, `debug`, `reload`, `runtime-decision`, `provision`, `stop`, `logs`, `self-test`, `device-target` and the CLI front door (`--adapter`/`--platform` validation now asks the registry) go from 90 adapter-name branches to 0; `src/` goes from 101 to 10 (one of the 101 was a comment, reworded).
16
+ - Platform code moves to `src/adapters/<adapter>/` (`overlay.ts`, `readiness.ts`, `doctor-checks.ts`, `fixtures.ts`, `reload.ts`, Mobile `devices.ts` and `prepare-target.ts`).
17
+ - The test-only `example` adapter now also installs, verifies, cleans up, records its runtime context, and is reached by doctor, fixtures, reload, runtime-decision, stop, provision and debug.
18
+ - Flags, output and the CLI goldens are unchanged for Mobile, Extension and Core. Four Terminal paths that fell into another platform's branch now behave as Terminal:
19
+ - `doctor` reports `cdp` as the screenshot provider instead of `simctl`/`adb`;
20
+ - `logs --window` names the tail window `browser-<port>` instead of `webpack-<port>`;
21
+ - `reload` and `fixtures set`/`reset` report that Terminal has nothing to reload or apply, instead of running the Mobile reload script or failing with a status-less envelope.
22
+
23
+ ### Fixed
24
+
25
+ - `doctor` finds leaked Metro bundlers by running `reap-checkout-metros.sh detect <checkout> [port]` with an argv instead of a `bash -c` string, so the checkout path and port are never shell text (code scanning alerts 41, 42 and 131).
26
+
27
+ ## 0.75.0 - 2026-10-04
28
+
29
+ ### Added
30
+
31
+ - `mm-harness pin` gives a task a harness that a global upgrade cannot change. It copies the running installed harness into an immutable, versioned store (`~/.local/share/mm-harness/pins/<version>-<hash>/`, APFS clone where available; `MM_HARNESS_PIN_ROOT` overrides) and prints the copy's executable for the task to record. Pins are keyed by the install's full content, so the same install returns the same pin and a same-version rebuild gets its own; absolute links into the install are re-pointed into the copy, and a pin whose content changed after it was created is refused rather than reused. A source checkout or an existing pin is printed as is; an install whose dependencies or links lead outside it is refused. `doctor` reports a pinned runner as `pinned-install`.
32
+
33
+ ### Changed
34
+
35
+ - `run` and `call` reach platform behavior through the adapter surface instead of branching on the adapter name: new optional `AdapterSurface` members `run` (environment, runtime preparation and checks, dependency block, heal wording, executed browser), `recording`, `diagnostics` (console capture, wallet log, in-app issue buffer), `observation` (network and performance) and `sourceFingerprint`. The engine modules (`run-engine`, `run`, `call`, `run-report`, `run-recording`, `recording-target`, `heal-bounds`, `execution-provenance`, `run-diagnostics`, performance and network observation, `cdp-trace`) go from 48 adapter-name branches to 0. Each platform's implementation lives under `src/adapters/<adapter>/` (`run.ts`, and now `launch.ts` for the Extension and Mobile launch profiles). The test-only `example` adapter is reached by the run/call engine with no shared-file edit. Flags, output, and the CLI goldens are unchanged, with one ordering exception: for `run` and `call` on Mobile, an invalid release-artifact state file is now reported after the running-recipe check instead of before it.
36
+ - The runner's action set comes from a required `AdapterSurface.actions` member instead of adapter-name branches: the bundled manifest, the semantic actions each platform bundles, when live adapter scripts run and under tsx, the `cdp.target` probe, the `ui.*` transport, wrappers around the action and `app.lifecycle` adapters, funded-mutation binding and recipe input checks. The device video recorder joins `AdapterSurface.recording`. `adapters.ts`, `runner.ts`, `metamask-action-validation.ts`, `live-adapter-contract.ts` and `manifest.ts` go from 45 adapter-name branches to 0. The Mobile bridge, HUD, native-provider, lifecycle and runtime-identity code moves to `src/adapters/mobile/` and loads on first use, as does the Extension page helper. The test-only `example` adapter runs a recipe through the runner (manifest, `cdp.target`, `ui.*`, a bundled action, a live script) with no shared-file edit. Flags, output, and the CLI goldens are unchanged.
37
+
38
+ ### Fixed
39
+
40
+ - `pr-body render` counts the nodes of a Recipe v1 graph (`workflow.nodes`). The recipe summary read `recipe.json (0 steps …)` for every v1 recipe; it now reads `recipe.json (<n> nodes …)` with the authored nodes (a `call` node once), next to the run log's executed node count.
41
+ - Several `mm-harness` commands starting at once no longer race on dependency links. The local Farmslot relink (`FARMSLOT_ROOT`, `dev:link-farmslot`) holds one `proper-lockfile` lock, reclaimed when its holder dies, and swaps each link with `rename()`, so runs no longer fail with `EEXIST`/`ENOENT` or see a package missing. The self-package link is renamed into place too, so a fresh install no longer gets a stray link inside the checkout.
42
+
5
43
  ## 0.74.0 - 2026-10-03
6
44
 
7
45
  ### Changed
8
46
 
9
- - Adapters register in an `AdapterRegistry` (`src/adapters/registry.ts`) instead of a compiled-in union: the adapter id is a string, the four built-in surfaces register themselves, and `--adapter` resolves through the registry. `launch` moves behind a new `AdapterSurface.launch` member, so the command keeps only the shared grammar and no longer branches on the adapter name (39 such branches in `commands/launch/index.ts`, now 0). A test-only `example` adapter registers and launches with no shared-file edit. Flags, output, and the CLI goldens are unchanged.
47
+ - Adapters register in an `AdapterRegistry` (`src/adapters/registry.ts`) instead of a compiled-in union: the adapter id is a string, the four built-in surfaces register themselves, and `launch` resolves `--adapter` through the registry (the CLI front door still accepts only the four built-ins). `launch` moves behind a new `AdapterSurface.launch` member, so the command keeps only the shared grammar and no longer branches on the adapter name (39 such branches in `commands/launch/index.ts`, now 0). A test-only `example` adapter registers and launches with no shared-file edit. Flags, output, and the CLI goldens are unchanged.
10
48
 
11
49
  ## 0.73.0 - 2026-10-03
12
50
 
package/README.md CHANGED
@@ -84,7 +84,7 @@ mm-harness fixtures init --from /secure/path/wallet-fixture.json
84
84
  mm-harness fixtures init --dev
85
85
  ```
86
86
 
87
- Use `mm-harness update` to update a published installation.
87
+ Use `mm-harness update` to update a published installation. `npm i -g` and `update` rewrite the global install in place, so a task that must survive an upgrade records `mm-harness pin` instead: it copies the running install into an immutable, versioned store and prints the copy's executable.
88
88
 
89
89
  ## Operate
90
90
 
@@ -30,10 +30,22 @@ deps_ready() {
30
30
  # is not npm-installed into its own node_modules, so symlink self for resolution.
31
31
  ensure_self_package_link() {
32
32
  local link="$RUNNER_DIR/node_modules/@deeeed/metamask-harness"
33
- /bin/mkdir -p "$RUNNER_DIR/node_modules/@deeeed"
34
- if [ ! -e "$link" ]; then
35
- ln -sf "$RUNNER_DIR" "$link"
33
+ if [ -e "$link" ]; then
34
+ return 0
36
35
  fi
36
+ # Several bins can start at once on a fresh install. `ln -sf` would follow a link another
37
+ # run just made and create a second one inside the runner checkout, so stage the link and
38
+ # rename it into place: rename() replaces a link without following it.
39
+ node -e '
40
+ const fs = require("node:fs");
41
+ const path = require("node:path");
42
+ const [runnerDir, link] = process.argv.slice(1);
43
+ fs.mkdirSync(path.dirname(link), { recursive: true });
44
+ const staged = `${link}.${process.pid}`;
45
+ fs.rmSync(staged, { force: true });
46
+ fs.symlinkSync(runnerDir, staged, "dir");
47
+ fs.renameSync(staged, link);
48
+ ' "$RUNNER_DIR" "$link"
37
49
  }
38
50
 
39
51
  ensure_dev_package_condition() {
@@ -6,6 +6,8 @@
6
6
  #
7
7
  # reap_checkout_metros <checkout-abs-path> [keep-pid]
8
8
  # detect_checkout_metros <checkout-abs-path> [live-port]
9
+ # Sourced for the functions; run directly, `detect <checkout-abs-path> [live-port]`
10
+ # prints the orphan pids (doctor calls it this way, with an argv and no shell string).
9
11
  # reap kills every bundler for the checkout (stop wants a clean slate), sparing
10
12
  # keep-pid. detect lists orphans for doctor and EXCLUDES the bundler serving
11
13
  # live-port (the currently-running valid Metro is not a leak).
@@ -73,3 +75,10 @@ detect_checkout_metros() {
73
75
  $(_checkout_metro_pids "$repo")
74
76
  LIST
75
77
  }
78
+
79
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
80
+ case "${1:-}" in
81
+ detect) shift; detect_checkout_metros "$@" ;;
82
+ *) printf 'usage: %s detect <checkout-abs-path> [live-port]\n' "$0" >&2; exit 2 ;;
83
+ esac
84
+ fi
@@ -0,0 +1,15 @@
1
+ function isRecord(value) {
2
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
3
+ }
4
+ function traceText(value) {
5
+ if (value === void 0 || value === null) return "";
6
+ if (value instanceof Error) return value.message;
7
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
8
+ return String(value);
9
+ }
10
+ return JSON.stringify(value);
11
+ }
12
+ export {
13
+ isRecord,
14
+ traceText
15
+ };
@@ -0,0 +1,20 @@
1
+ import { manifestPath } from "../../paths.js";
2
+ const coreActions = {
3
+ manifestPath: () => manifestPath("core"),
4
+ semantic: [
5
+ "metamask.perps.close_positions",
6
+ "metamask.perps.start_state",
7
+ "metamask.perps.read_snapshot",
8
+ "metamask.perps.read_account"
9
+ ],
10
+ // read_account is Core's alone, and only its live adapter implements it.
11
+ liveOnly: ["metamask.perps.read_account"],
12
+ tsxLiveScripts: {
13
+ env: async (projectRoot, tempDir) => (await import("./live-script-env.js")).coreLiveScriptEnv(projectRoot, tempDir)
14
+ },
15
+ // Core declares no cdp.target; the probe matches the Extension's.
16
+ cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" }
17
+ };
18
+ export {
19
+ coreActions
20
+ };
@@ -0,0 +1,36 @@
1
+ import { existsSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { corePnpNodeOptions } from "../../commands/core-readiness.js";
5
+ const CORE_WORKSPACE_PACKAGES = [
6
+ "@metamask/base-controller",
7
+ "@metamask/messenger",
8
+ "@metamask/controller-utils",
9
+ "@metamask/keyring-controller"
10
+ ];
11
+ async function coreLiveScriptEnv(projectRoot, tempDir) {
12
+ const paths = {};
13
+ for (const pkg of CORE_WORKSPACE_PACKAGES) {
14
+ const packageDir = path.join(projectRoot, "packages", pkg.replace("@metamask/", ""));
15
+ const distEntry = path.join(packageDir, "dist/index.cjs");
16
+ if (existsSync(distEntry)) continue;
17
+ const srcDir = path.join(packageDir, "src");
18
+ if (!existsSync(path.join(srcDir, "index.ts"))) continue;
19
+ paths[pkg] = [path.join(srcDir, "index.ts")];
20
+ paths[`${pkg}/*`] = [path.join(srcDir, "*")];
21
+ }
22
+ if (Object.keys(paths).length === 0) return {};
23
+ const tsconfigPath = path.join(tempDir, "core-adapter.tsconfig.json");
24
+ await writeFile(
25
+ tsconfigPath,
26
+ `${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
27
+ `
28
+ );
29
+ return {
30
+ TSX_TSCONFIG_PATH: tsconfigPath,
31
+ NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
32
+ };
33
+ }
34
+ export {
35
+ coreLiveScriptEnv
36
+ };
@@ -0,0 +1,34 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { recipeHarnessPath } from "../../paths.js";
4
+ function isExecutable(file) {
5
+ try {
6
+ const stat = fs.statSync(file);
7
+ return stat.isFile() && (stat.mode & 73) !== 0;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+ const coreHarness = {
13
+ install: { entry: "adapters/core/inject.sh", fallback: "scripts/inject-core-harness.sh" },
14
+ cleanup: { entry: "adapters/core/cleanup.sh", fallback: "scripts/cleanup-core-harness.sh", fromInstalledRunner: true },
15
+ verify(target) {
16
+ const delegate = path.join(recipeHarnessPath(target, "core"), "runner", "bin", "mm-harness");
17
+ if (!isExecutable(delegate)) {
18
+ return {
19
+ error: `core recipe harness not installed: missing delegate ${delegate}.
20
+ Next: mm-harness install --platform core --target ${target}`
21
+ };
22
+ }
23
+ return { command: delegate, prefixArgs: ["doctor", "--adapter", "core"] };
24
+ }
25
+ };
26
+ const coreRuntimeContext = {
27
+ forbiddenFields: ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"]
28
+ };
29
+ const corePrepare = {};
30
+ export {
31
+ coreHarness,
32
+ corePrepare,
33
+ coreRuntimeContext
34
+ };
@@ -0,0 +1,8 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const coreReadiness = {
4
+ compatibilityMode: (target) => fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge"
5
+ };
6
+ export {
7
+ coreReadiness
8
+ };
@@ -0,0 +1,54 @@
1
+ import { coreDependencyBlock } from "../../commands/core-readiness.js";
2
+ import { isRecord } from "../../commands/parse-args.js";
3
+ const coreRun = {
4
+ async dependencyBlock(target, { recipe, librarySources, action }) {
5
+ const usesController = action !== void 0 ? actionUsesCoreController(action) : await recipeUsesCoreController(recipe, librarySources);
6
+ return usesController ? coreDependencyBlock(target) : null;
7
+ },
8
+ violationUserAction: (violation) => violation.code === "WALLET_STATE_REQUIRED" ? "for signing, configure a reserved wallet-fixture.json account and pass its non-empty name; an explicit 0x address is supported only by read-only actions" : void 0
9
+ };
10
+ async function recipeUsesCoreController(recipe, librarySources) {
11
+ if (objectUsesCoreController(recipe)) return true;
12
+ const refs = collectCallRefs(recipe);
13
+ if (refs.length === 0) return false;
14
+ const sources = librarySources ?? [];
15
+ if (sources.length === 0) return false;
16
+ const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
17
+ const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
18
+ const visited = /* @__PURE__ */ new Set();
19
+ const visit = (ref) => {
20
+ if (visited.has(ref)) return false;
21
+ visited.add(ref);
22
+ const dependency = recipes.get(ref);
23
+ if (!dependency) return false;
24
+ if (objectUsesCoreController(dependency.document)) return true;
25
+ return collectCallRefs(dependency.document).some(visit);
26
+ };
27
+ return refs.some(visit);
28
+ }
29
+ function objectUsesCoreController(value) {
30
+ if (Array.isArray(value)) return value.some(objectUsesCoreController);
31
+ if (!isRecord(value)) return false;
32
+ if (actionUsesCoreController(value.action)) return true;
33
+ return Object.values(value).some(objectUsesCoreController);
34
+ }
35
+ function collectCallRefs(value) {
36
+ const refs = [];
37
+ const visit = (node) => {
38
+ if (Array.isArray(node)) {
39
+ for (const item of node) visit(item);
40
+ return;
41
+ }
42
+ if (!isRecord(node)) return;
43
+ if (node.action === "call" && typeof node.ref === "string" && node.ref) refs.push(node.ref);
44
+ for (const child of Object.values(node)) visit(child);
45
+ };
46
+ visit(value);
47
+ return [...new Set(refs)];
48
+ }
49
+ function actionUsesCoreController(action) {
50
+ return typeof action === "string" && action.startsWith("metamask.perps.");
51
+ }
52
+ export {
53
+ coreRun
54
+ };
@@ -2,6 +2,10 @@ import path from "node:path";
2
2
  import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
3
3
  import { shellQuote } from "../../commands/parse-args.js";
4
4
  import { adapterRegistry } from "../registry.js";
5
+ import { coreActions } from "./actions.js";
6
+ import { coreHarness, corePrepare, coreRuntimeContext } from "./overlay.js";
7
+ import { coreReadiness } from "./readiness.js";
8
+ import { coreRun } from "./run.js";
5
9
  const coreSurface = {
6
10
  adapter: "core",
7
11
  headless: true,
@@ -55,7 +59,14 @@ const coreSurface = {
55
59
  relaunch: "mm-harness verify",
56
60
  runtimeProbeRecovery: (target) => `mm-harness verify --adapter core --target ${shellQuote(path.resolve(target))}`
57
61
  },
58
- launch: async ({ usage }) => usage("core is headless; there is nothing to launch.", "mm-harness verify")
62
+ launch: async ({ usage }) => usage("core is headless; there is nothing to launch.", "mm-harness verify"),
63
+ actions: coreActions,
64
+ harness: coreHarness,
65
+ runtimeContext: coreRuntimeContext,
66
+ readiness: coreReadiness,
67
+ fixtures: { operations: ["init", "sync", "set"] },
68
+ prepare: corePrepare,
69
+ run: coreRun
59
70
  };
60
71
  adapterRegistry.register(coreSurface);
61
72
  export {
@@ -0,0 +1,206 @@
1
+ function isRecord(value) {
2
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ function extensionActionInputFindings(nodeId, node) {
5
+ const findings = [];
6
+ const action = node.action;
7
+ const maximum = (name, limit) => {
8
+ if (typeof node[name] !== "number" || node[name] <= limit) return;
9
+ findings.push({
10
+ severity: "error",
11
+ code: "recipe.invalid_param",
12
+ path: `workflow.nodes.${nodeId}.${name}`,
13
+ message: `${action} ${name} must not exceed ${limit}`
14
+ });
15
+ };
16
+ if (action === "metamask.perps.read_snapshot") {
17
+ if (node.market !== void 0 && node.markets !== void 0) {
18
+ findings.push({
19
+ severity: "error",
20
+ code: "recipe.invalid_param",
21
+ path: `workflow.nodes.${nodeId}.markets`,
22
+ message: `${action} accepts market or markets, not both`
23
+ });
24
+ }
25
+ if (isRecord(node.history)) {
26
+ const start = node.history.start_time_ms;
27
+ const end = node.history.end_time_ms;
28
+ if (typeof start === "number" && typeof end === "number" && start > end) {
29
+ findings.push({
30
+ severity: "error",
31
+ code: "recipe.invalid_param",
32
+ path: `workflow.nodes.${nodeId}.history.start_time_ms`,
33
+ message: `${action} history.start_time_ms must not exceed history.end_time_ms`
34
+ });
35
+ }
36
+ if (typeof node.history.limit === "number" && node.history.limit > 500) {
37
+ findings.push({
38
+ severity: "error",
39
+ code: "recipe.invalid_param",
40
+ path: `workflow.nodes.${nodeId}.history.limit`,
41
+ message: `${action} history.limit must not exceed 500`
42
+ });
43
+ }
44
+ }
45
+ if (typeof node.max_items_per_section === "number" && node.max_items_per_section > 500) {
46
+ findings.push({
47
+ severity: "error",
48
+ code: "recipe.invalid_param",
49
+ path: `workflow.nodes.${nodeId}.max_items_per_section`,
50
+ message: `${action} max_items_per_section must not exceed 500`
51
+ });
52
+ }
53
+ if (typeof node.timeout_ms === "number" && node.timeout_ms > 6e4) {
54
+ findings.push({
55
+ severity: "error",
56
+ code: "recipe.invalid_param",
57
+ path: `workflow.nodes.${nodeId}.timeout_ms`,
58
+ message: `${action} timeout_ms must not exceed 60000`
59
+ });
60
+ }
61
+ if (isRecord(node.sampling)) {
62
+ if (typeof node.sampling.count === "number" && node.sampling.count > 10) {
63
+ findings.push({
64
+ severity: "error",
65
+ code: "recipe.invalid_param",
66
+ path: `workflow.nodes.${nodeId}.sampling.count`,
67
+ message: `${action} sampling.count must not exceed 10`
68
+ });
69
+ }
70
+ if (typeof node.sampling.interval_ms === "number" && node.sampling.interval_ms > 3e4) {
71
+ findings.push({
72
+ severity: "error",
73
+ code: "recipe.invalid_param",
74
+ path: `workflow.nodes.${nodeId}.sampling.interval_ms`,
75
+ message: `${action} sampling.interval_ms must not exceed 30000`
76
+ });
77
+ }
78
+ }
79
+ if (isRecord(node.expect)) {
80
+ if (typeof node.expect.minimum_changed_market_count === "number" && node.expect.minimum_changed_market_count > 100) {
81
+ findings.push({
82
+ severity: "error",
83
+ code: "recipe.invalid_param",
84
+ path: `workflow.nodes.${nodeId}.expect.minimum_changed_market_count`,
85
+ message: `${action} expect.minimum_changed_market_count must not exceed 100`
86
+ });
87
+ }
88
+ if (typeof node.expect.minimum_history_count === "number" && node.expect.minimum_history_count > 500) {
89
+ findings.push({
90
+ severity: "error",
91
+ code: "recipe.invalid_param",
92
+ path: `workflow.nodes.${nodeId}.expect.minimum_history_count`,
93
+ message: `${action} expect.minimum_history_count must not exceed 500`
94
+ });
95
+ }
96
+ }
97
+ }
98
+ if (action === "metamask.perps.assert_visible_consistency") {
99
+ maximum("sample_count", 10);
100
+ maximum("sample_interval_ms", 3e4);
101
+ maximum("minimum_changed_count", 200);
102
+ maximum("minimum_recent_activity_count", 50);
103
+ maximum("timeout_ms", 12e4);
104
+ maximum("max_items", 200);
105
+ }
106
+ const fundedVisibleMutation = action === "metamask.perps.close_visible_position" || action === "metamask.perps.close_orders" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl" || action === "metamask.perps.place_order" && node.confirm_live === true;
107
+ if (fundedVisibleMutation) {
108
+ const required = (name, pattern, message) => {
109
+ if (typeof node[name] === "string" && pattern.test(node[name])) return;
110
+ findings.push({
111
+ severity: "error",
112
+ code: "recipe.invalid_param",
113
+ path: `workflow.nodes.${nodeId}.${name}`,
114
+ message
115
+ });
116
+ };
117
+ const decimal = action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? node.funding_amount : node.amount ?? node.notional;
118
+ const decimalText = String(decimal ?? "");
119
+ const match = /^([1-9][0-9]*)(?:\.([0-9]{1,6}))?$/u.exec(decimalText);
120
+ if (!match) {
121
+ findings.push({
122
+ severity: "error",
123
+ code: "recipe.invalid_param",
124
+ path: `workflow.nodes.${nodeId}.${action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? "funding_amount" : "amount"}`,
125
+ message: `${action} funded decimal amount must be positive plain decimal text with at most six places`
126
+ });
127
+ }
128
+ if (action === "metamask.perps.place_order" && node.submit_attempts !== void 0 && node.submit_attempts !== 1) {
129
+ findings.push({
130
+ severity: "error",
131
+ code: "recipe.invalid_param",
132
+ path: `workflow.nodes.${nodeId}.submit_attempts`,
133
+ message: `${action} funded visible submission requires submit_attempts=1`
134
+ });
135
+ }
136
+ if (action === "metamask.perps.close_visible_position" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
137
+ required("funding_side", /^(?:long|short)$/u, `${action} requires the authorized position side`);
138
+ }
139
+ if (action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
140
+ required(
141
+ "funding_position_size",
142
+ /^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
143
+ `${action} requires the exact visible position size`
144
+ );
145
+ }
146
+ if (action === "metamask.perps.close_visible_position" && node.cleanup_only === true) {
147
+ required(
148
+ "funding_position_size",
149
+ /^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
150
+ `${action} cleanup requires the exact visible position size`
151
+ );
152
+ if (node.amount != null) {
153
+ findings.push({
154
+ severity: "error",
155
+ code: "recipe.invalid_param",
156
+ path: `workflow.nodes.${nodeId}.amount`,
157
+ message: `${action} cleanup must close the full exact position`
158
+ });
159
+ }
160
+ }
161
+ if (action === "metamask.perps.edit_margin" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.funding_amount ?? ""))) {
162
+ findings.push({
163
+ severity: "error",
164
+ code: "recipe.invalid_param",
165
+ path: `workflow.nodes.${nodeId}.funding_amount`,
166
+ message: `${action} requires the authorized position amount`
167
+ });
168
+ }
169
+ if (action === "metamask.perps.close_orders") {
170
+ required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
171
+ const outputPrice = /^\{\{outputs\.[A-Za-z0-9_.-]+\}\}$/u.test(String(node.limit_price ?? ""));
172
+ if (!outputPrice && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
173
+ findings.push({
174
+ severity: "error",
175
+ code: "recipe.invalid_param",
176
+ path: `workflow.nodes.${nodeId}.limit_price`,
177
+ message: `${action} requires an explicit positive limit_price for resumable cancellation`
178
+ });
179
+ }
180
+ }
181
+ if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
182
+ findings.push({
183
+ severity: "error",
184
+ code: "recipe.invalid_param",
185
+ path: `workflow.nodes.${nodeId}.limit_price`,
186
+ message: `${action} funded limit submission requires explicit limit_price; offset-only pricing is not resumable`
187
+ });
188
+ }
189
+ for (const name of ["identity_rounding_tolerance_base_units", "balance_reconciliation_tolerance_base_units"]) {
190
+ if (node[name] === void 0) continue;
191
+ const value = String(node[name]);
192
+ if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(value) || BigInt(value) > 1000000n) {
193
+ findings.push({
194
+ severity: "error",
195
+ code: "recipe.invalid_param",
196
+ path: `workflow.nodes.${nodeId}.${name}`,
197
+ message: `${action} ${name} must be an integer string from 0 through 1000000`
198
+ });
199
+ }
200
+ }
201
+ }
202
+ return findings;
203
+ }
204
+ export {
205
+ extensionActionInputFindings
206
+ };
@@ -0,0 +1,51 @@
1
+ import { manifestPath } from "../../paths.js";
2
+ import { extensionActionInputFindings } from "./action-inputs.js";
3
+ const extensionActions = {
4
+ manifestPath: () => manifestPath("extension"),
5
+ semantic: [
6
+ "app.network_capture",
7
+ "app.network_assert",
8
+ "app.performance_capture",
9
+ "app.performance_assert",
10
+ "metamask.assets.prepare_send",
11
+ "metamask.assets.review_send",
12
+ "metamask.assets.set_custom_gas",
13
+ "metamask.assets.select_network_scope",
14
+ "metamask.networks.add_chainlist",
15
+ "metamask.perps.compare_provider_market",
16
+ "metamask.perps.read_eligibility",
17
+ "metamask.perps.open_position_action",
18
+ "metamask.perps.edit_margin",
19
+ "metamask.perps.update_position_tpsl",
20
+ "metamask.perps.assert_visible_consistency",
21
+ "metamask.settings.set_basic_functionality",
22
+ "metamask.perps.read_snapshot"
23
+ ],
24
+ liveNeedsCdpPort: true,
25
+ cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" },
26
+ ui: (harness) => ({
27
+ base: harness.createCdpWebUiTransport({
28
+ async withPage(input, callback) {
29
+ const { withExtensionPage } = await import("../../../library/actions/extension/platform/cdp.mjs");
30
+ return withExtensionPage(
31
+ {
32
+ action: input.action,
33
+ node: input.node,
34
+ context: {
35
+ nodeId: input.context.nodeId,
36
+ projectRoot: input.context.projectRoot,
37
+ artifactsDir: input.context.artifactsDir
38
+ }
39
+ },
40
+ callback
41
+ );
42
+ }
43
+ })
44
+ }),
45
+ trustedMutation: true,
46
+ inputFindings: extensionActionInputFindings,
47
+ ensureChecksOnly: true
48
+ };
49
+ export {
50
+ extensionActions
51
+ };
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
1
3
  import { recipeRuntimePath } from "../../paths.js";
2
4
  import { ensureConsoleCollector } from "../console-capture.js";
3
5
  function extensionCaptureFiles(target) {
@@ -25,8 +27,18 @@ async function ensureExtensionConsoleCapture(target) {
25
27
  cwd: target
26
28
  });
27
29
  }
30
+ async function verifyExtensionConsoleControl(capture) {
31
+ let extensionId = null;
32
+ try {
33
+ extensionId = fs.readFileSync(recipeRuntimePath(capture.projectRoot, "extension.id"), "utf8").trim();
34
+ } catch {
35
+ extensionId = null;
36
+ }
37
+ return verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
38
+ }
28
39
  export {
29
40
  ensureExtensionConsoleCapture,
30
41
  extensionCaptureFiles,
31
- extensionCdpPort
42
+ extensionCdpPort,
43
+ verifyExtensionConsoleControl
32
44
  };