@deeeed/metamask-harness 0.92.0 → 0.92.2

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,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.92.2 - 2026-10-14
6
+
7
+ ### Fixed
8
+
9
+ - Check console errors within the current recipe run when reusing a browser, preserving prior logs and still rejecting new errors.
10
+
11
+ ## 0.92.1 - 2026-10-14
12
+
13
+ ### Fixed
14
+
15
+ - Use the coordinated recipe-runner 0.34.1 dependency release so recorded UI runs bind the browser after setup, with one engine version across adapters and recipe hosts.
16
+
17
+
5
18
  ## 0.92.0 - 2026-10-14
6
19
 
7
20
  ### Changed
@@ -1,6 +1,8 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
5
+ import { RUN_CONSOLE_WINDOW_ENV } from "../../library/actions/shared/console-assert.mjs";
4
6
  import {
5
7
  collectorMatches,
6
8
  collectorOwnerMarker,
@@ -13,6 +15,51 @@ import {
13
15
  readPid
14
16
  } from "../../library/actions/shared/console-collector.mjs";
15
17
  import { runnerDir } from "../paths.js";
18
+ import { isRecord } from "../action-values.js";
19
+ function createRunConsoleWindow(captureFiles) {
20
+ const current = new AsyncLocalStorage();
21
+ const wrapAdapters = (adapters) => adapters.map((entry) => ({
22
+ ...entry,
23
+ execute(node, context) {
24
+ const env = { ...context.env };
25
+ delete env[RUN_CONSOLE_WINDOW_ENV];
26
+ const boundary = current.getStore();
27
+ if (boundary) env[RUN_CONSOLE_WINDOW_ENV] = boundary;
28
+ return entry.execute(node, { ...context, env });
29
+ }
30
+ }));
31
+ const wrapRunner = (runner) => ({
32
+ preflight: (request) => runner.preflight(request),
33
+ async run(request) {
34
+ const { DEFAULT_RECIPE_RUNTIME_DIR, harnessHost, recipeRuntimePath, validateRelativeRecipePath } = await import("@farmslot/recipe-cli/harness");
35
+ if (isStandaloneCall(request, harnessHost().name)) return current.run(void 0, () => runner.run(request));
36
+ const projectRoot = fs.realpathSync(path.resolve(request.projectRoot ?? process.cwd()));
37
+ const startedAtMs = Date.now();
38
+ const { pageLog, extensionLog } = captureFiles(projectRoot);
39
+ const env = { ...process.env, ...request.env };
40
+ const runtime = path.join(projectRoot, validateRelativeRecipePath("RECIPE_RUNTIME_DIR", env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR));
41
+ const parentRuntime = recipeRuntimePath(projectRoot);
42
+ const files = Object.fromEntries([pageLog, extensionLog].map((original) => {
43
+ const file = path.join(runtime, path.relative(parentRuntime, original));
44
+ try {
45
+ const stat = fs.statSync(file);
46
+ return [file, { offset: stat.size, inode: stat.ino }];
47
+ } catch (error) {
48
+ if (error.code !== "ENOENT") throw error;
49
+ return [file, { offset: 0 }];
50
+ }
51
+ }));
52
+ return current.run(JSON.stringify({ projectRoot, startedAtMs, files }), () => runner.run(request));
53
+ }
54
+ });
55
+ return { wrapAdapters, wrapRunner };
56
+ }
57
+ function isStandaloneCall(request, host) {
58
+ const document = request.recipeDocument;
59
+ if (request.recipePath || !isRecord(document) || !isRecord(document.workflow)) return false;
60
+ const nodes = document.workflow.nodes;
61
+ return isRecord(nodes) && isRecord(nodes.call) && isRecord(nodes.done) && Object.keys(nodes).length === 2 && document.workflow.entry === "call" && document.title === `${host} call ${nodes.call.action}` && nodes.call.next === "done" && nodes.done.action === "end";
62
+ }
16
63
  const COLLECTOR_SCRIPT = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
17
64
  async function ensureConsoleCollector(options) {
18
65
  const { cdpPort, extensionLog, pageLog, collectorLog, pidFile } = options;
@@ -132,6 +179,7 @@ function containsAttachment(from) {
132
179
  export {
133
180
  COLLECTOR_SCRIPT,
134
181
  ROTATE_LOG_BYTES,
182
+ createRunConsoleWindow,
135
183
  ensureConsoleCollector,
136
184
  verifyOwnedCollectorControl
137
185
  };
package/dist/runner.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
4
4
  import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
5
5
  import { getAdapterSurface } from "./adapters/surface.js";
6
+ import { createRunConsoleWindow } from "./adapters/console-capture.js";
6
7
  import {
7
8
  bindMetaMaskMobileLifecycleDeadline,
8
9
  createMetaMaskMobileLifecycleCommandRunner,
@@ -113,12 +114,15 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
113
114
  ...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
114
115
  }));
115
116
  const mutationBound = surface.actions.trustedMutation ? withTrustedMutationContext(secured, options.trustedMutation, externalMutationActions) : secured;
116
- const runner = createRecipeRunner({
117
+ const consoleCapture = surface.diagnostics?.console;
118
+ const consoleWindow = consoleCapture ? createRunConsoleWindow((projectRoot) => consoleCapture.files(projectRoot)) : void 0;
119
+ const adapters = withActionProgress(
120
+ surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
121
+ options.onActionEvent
122
+ );
123
+ const engineRunner = createRecipeRunner({
117
124
  actionManifest,
118
- adapters: withActionProgress(
119
- surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
120
- options.onActionEvent
121
- ),
125
+ adapters: consoleWindow ? consoleWindow.wrapAdapters(adapters) : adapters,
122
126
  logger,
123
127
  recording: {
124
128
  targetProvider: createRecordingTargetProvider(adapter),
@@ -141,6 +145,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
141
145
  }
142
146
  }
143
147
  });
148
+ const runner = consoleWindow ? consoleWindow.wrapRunner(engineRunner) : engineRunner;
144
149
  const clearHud = surface.actions.clearHud;
145
150
  if (options.autoHud !== false || hudOffSwitch || !clearHud) return runner;
146
151
  return {
package/docs/RECIPES.md CHANGED
@@ -22,8 +22,9 @@ and shadows. Use `--json` for exact metadata.
22
22
  Before ticket work, run `mm-harness run health.check`. It is read-only and
23
23
  picks the variant for the current adapter. Mobile and Extension check that the
24
24
  app answers, onboarding is done, the wallet is unlocked on a selected account
25
- at wallet home, and the app logged no console error since launch outside the
26
- console allowlists, then take one screenshot. Core reads one HyperLiquid
25
+ at wallet home, and the app logged no console error outside the console
26
+ allowlists, then take one screenshot. Extension checks this recipe run;
27
+ Mobile retains its launch window. Core reads one HyperLiquid
27
28
  testnet account through the Perps controller and requires a numeric balance.
28
29
  A failure stops at a node named after the error (`APP_NOT_RUNNING`,
29
30
  `ONBOARDING`, `WALLET_LOCKED`, `NO_SELECTED_ACCOUNT`, `NOT_ON_HOME`,
@@ -26,7 +26,7 @@ import { pathToFileURL } from 'node:url';
26
26
 
27
27
  import { recipeRuntimePath } from '@farmslot/recipe-cli/harness';
28
28
 
29
- import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, readLaunchMark, reportVerdict, throwOnFindings } from '../../shared/console-assert.mjs';
29
+ import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, readLaunchMark, reportVerdict, runConsoleMark, throwOnFindings } from '../../shared/console-assert.mjs';
30
30
  import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
31
31
  import { runAdapter } from '../platform/cdp.mjs';
32
32
 
@@ -68,14 +68,15 @@ export async function assertNoConsoleErrors(input, env = process.env, deps = {})
68
68
  ...(node.timeout_ms ? { timeoutMs: Number(node.timeout_ms) } : {}),
69
69
  });
70
70
  if (!control.ok) throw unavailable(control.detail);
71
- const mark = node.since === 'start-of-log' ? null : readLaunchMark(recipeRuntimePath(projectRoot, LAUNCH_MARK), unavailable);
71
+ const runMark = runConsoleMark(input, env, log);
72
+ const mark = runMark ?? (node.since === 'start-of-log' ? null : readLaunchMark(recipeRuntimePath(projectRoot, LAUNCH_MARK), unavailable));
72
73
  const { text, reset } = readFrom(log, mark ?? { offset: 0 });
73
74
  const allowlist = nodeAllowlist(node, env);
74
75
  const keep = inWindow({ sinceMs: mark?.launchedAtMs ?? null, now: deps.now ?? Date.now() });
75
76
  const { findings, allowlisted } = judgeConsole([text], { levels, allowlist, keep });
76
77
  const output = {
77
78
  action: input.action,
78
- since: node.since ?? 'launch',
79
+ since: runMark ? 'run' : node.since ?? 'launch',
79
80
  levels: [...levels],
80
81
  findings,
81
82
  allowlisted,
@@ -4,7 +4,7 @@
4
4
  // (unallowlisted errors, capture not proven). Each adapter's action proves its
5
5
  // own capture and window, then calls these.
6
6
 
7
- import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs';
7
+ import { closeSync, fstatSync, openSync, readFileSync, realpathSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
 
10
10
  import {
@@ -17,6 +17,20 @@ import {
17
17
  withBundledLibraryRoot,
18
18
  } from './console-findings.mjs';
19
19
 
20
+ // Private run-local state passed to action subprocesses, never a slot-wide mark.
21
+ export const RUN_CONSOLE_WINDOW_ENV = 'METAMASK_RECIPE_RUN_CONSOLE_WINDOW';
22
+
23
+ export function runConsoleMark(input, env, file) {
24
+ if (input.node?.since === 'start-of-log' || !env[RUN_CONSOLE_WINDOW_ENV]) return null;
25
+ const window = JSON.parse(env[RUN_CONSOLE_WINDOW_ENV]);
26
+ const captured = window.files?.[realpathSync(file)];
27
+ if (window.projectRoot !== realpathSync(path.resolve(input.context.projectRoot)) ||
28
+ !Number.isFinite(window.startedAtMs) || !captured) {
29
+ throw captureUnavailable('the run console boundary does not match this capture');
30
+ }
31
+ return { ...captured, launchedAtMs: window.startedAtMs };
32
+ }
33
+
20
34
  // The capture from a recorded offset. A file now shorter than the offset, or
21
35
  // a different file, was replaced since: read it whole and say so, rather than
22
36
  // reading nothing.
@@ -1,8 +1,8 @@
1
1
  // Fail the recipe when the app page logged an error or uncaught exception
2
2
  // that the recipe libraries' console allowlist does not mark as known
3
3
  // third-party noise. Reads the slot console capture (app-console.log, and
4
- // extension-console.log when asked) from where the current slot browser's
5
- // launch began. The same classifier and allowlist feed run diagnostics.
4
+ // extension-console.log when asked) from this recipe run's boundary, or the
5
+ // browser launch for standalone actions. The classifier also feeds diagnostics.
6
6
  // It never passes on missing evidence: the slot's own console collector must
7
7
  // be running on the browser's CDP port, and for every capture file a
8
8
  // requested source reads, a control line logged in its target (the app page,
@@ -11,7 +11,7 @@
11
11
 
12
12
  import path from 'node:path';
13
13
 
14
- import { captureUnavailable, consoleLevels, judgeConsole, nodeAllowlist, readFrom, throwOnFindings } from '../../shared/console-assert.mjs';
14
+ import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, runConsoleMark, throwOnFindings } from '../../shared/console-assert.mjs';
15
15
  import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
16
16
  import { isDirectRun, readBrowserState, runAdapter, webDappPolicy, webDappRuntimeDir } from '../platform/runtime.mjs';
17
17
 
@@ -49,14 +49,22 @@ export async function assertNoConsoleErrors(input, env = process.env, deps = {})
49
49
  const allowlist = nodeAllowlist(node, env);
50
50
  const texts = [];
51
51
  const offsetResets = [];
52
+ let runMark = null;
52
53
  for (const file of files) {
53
- const { text, reset } = readFrom(path.join(runtime, LOGS[file]), { offset: offsets[file] ?? 0, inode: offsets.inodes?.[file] });
54
+ const capturePath = path.join(runtime, LOGS[file]);
55
+ const mark = runConsoleMark(input, env, capturePath);
56
+ runMark ??= mark;
57
+ const { text, reset } = readFrom(capturePath, mark ?? { offset: offsets[file] ?? 0, inode: offsets.inodes?.[file] });
54
58
  if (reset) offsetResets.push(file);
55
59
  texts.push(text);
56
60
  }
57
- const { findings, allowlisted } = judgeConsole(texts, { levels, allowlist, accept: (finding) => sources.includes(finding.source) });
61
+ const { findings, allowlisted } = judgeConsole(texts, {
62
+ levels, allowlist, accept: (finding) => sources.includes(finding.source),
63
+ keep: inWindow({ sinceMs: runMark?.launchedAtMs ?? null, now: deps.now ?? Date.now() }),
64
+ });
58
65
  const output = {
59
66
  action: input.action,
67
+ since: runMark ? 'run' : node.since ?? 'launch',
60
68
  sources,
61
69
  levels: [...levels],
62
70
  findings,
@@ -1244,7 +1244,7 @@
1244
1244
  "execution_capabilities": []
1245
1245
  },
1246
1246
  "metamask.app.assert_no_console_errors": {
1247
- "description": "Fail when MetaMask (service worker or UI pages) logged an error or uncaught exception that no console allowlist (the bundled library's, then each recipe library's diagnostics/console-allowlist.json) marks as known noise, judging the events since the launch began, by their own timestamps. Reads extension-console.log from the offset mm-harness launch records when it starts, and leaves out records the collector stamps with an event time before the launch (a replaced collector's replay of a browser the launch kept); with no recorded launch it fails unless since is start-of-log. The same classification feeds run diagnostics. Fails as unavailable unless this runtime's console collector runs on the run's CDP port and a control line logged in MetaMask's service worker or page reaches extension-console.log. Allowlist entries are exact signatures (schemaVersion 3).",
1247
+ "description": "Fail when MetaMask (service worker or UI pages) logged an error or uncaught exception that no console allowlist (the bundled library's, then each recipe library's diagnostics/console-allowlist.json) marks as known noise, judging events during the current recipe run by their own timestamps, or since launch for a standalone action. Reads extension-console.log from the run boundary, or the offset mm-harness launch records when it starts, and leaves out records the collector stamps with an event time before the launch (a replaced collector's replay of a browser the launch kept); a standalone action with no recorded launch fails unless since is start-of-log. The same classification feeds run diagnostics. Fails as unavailable unless this runtime's console collector runs on the run's CDP port and a control line logged in MetaMask's service worker or page reaches extension-console.log. Allowlist entries are exact signatures (schemaVersion 3).",
1248
1248
  "execution_capabilities": ["host-read-export"],
1249
1249
  "schema": {
1250
1250
  "type": "object",
@@ -502,7 +502,7 @@
502
502
  ]
503
503
  },
504
504
  "metamask.app.assert_no_console_errors": {
505
- "description": "Fail when the app page logged an error or uncaught exception since the slot browser's launch that no recipe-library console allowlist (diagnostics/console-allowlist.json) marks as known third-party noise. Reads the slot console capture; the same classification feeds run diagnostics. Fails as unavailable unless the slot's own console collector runs on the browser's CDP port and, for each capture file a requested source reads, a control line logged in its target (the app page; MetaMask for the extension source) reaches that file. Allowlist entries are exact signatures (schemaVersion 3).",
505
+ "description": "Fail when the app page logged an error or uncaught exception during the current recipe run, or since the slot browser's launch for a standalone action that no recipe-library console allowlist (diagnostics/console-allowlist.json) marks as known third-party noise. Reads the slot console capture; the same classification feeds run diagnostics. Fails as unavailable unless the slot's own console collector runs on the browser's CDP port and, for each capture file a requested source reads, a control line logged in its target (the app page; MetaMask for the extension source) reaches that file. Allowlist entries are exact signatures (schemaVersion 3).",
506
506
  "execution_capabilities": [
507
507
  "host-read-export"
508
508
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.92.0",
3
+ "version": "0.92.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,15 +21,15 @@
21
21
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
22
22
  },
23
23
  "dependencies": {
24
- "@farmslot/adapter-node": "0.1.17",
25
- "@farmslot/adapter-rn": "0.22.1",
26
- "@farmslot/adapter-sdk": "0.12.0",
27
- "@farmslot/adapter-web": "0.8.2",
28
- "@farmslot/agent-runtime": "0.20.1",
24
+ "@farmslot/adapter-node": "0.1.18",
25
+ "@farmslot/adapter-rn": "0.22.2",
26
+ "@farmslot/adapter-sdk": "0.12.1",
27
+ "@farmslot/adapter-web": "0.8.3",
28
+ "@farmslot/agent-runtime": "0.21.1",
29
29
  "@farmslot/handoff": "^0.3.1",
30
30
  "@farmslot/protocol": "0.43.0",
31
- "@farmslot/recipe-cli": "0.16.1",
32
- "@farmslot/recipe-runner": "0.34.0",
31
+ "@farmslot/recipe-cli": "0.16.3",
32
+ "@farmslot/recipe-runner": "0.34.1",
33
33
  "agent-device": "0.19.3",
34
34
  "commander": "^12.0.0",
35
35
  "esbuild": "0.28.1",