@deeeed/metamask-harness 0.82.1 → 0.84.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 +48 -0
- package/README.md +8 -1
- package/adapters/core/cleanup.sh +5 -29
- package/adapters/core/controller-import-probe.mjs +26 -0
- package/adapters/manifest.json +22 -102
- package/adapters/shared/session-lock.mjs +43 -0
- package/adapters/web-dapp/metamask-signer.mjs +212 -0
- package/dist/adapters/core/actions.js +0 -3
- package/dist/adapters/core/overlay.js +1 -5
- package/dist/adapters/core/readiness.js +133 -2
- package/dist/adapters/core/surface.js +53 -41
- package/dist/adapters/extension/extension-id.js +1 -0
- package/dist/adapters/extension/fixtures.js +60 -1
- package/dist/adapters/extension/network-observer.js +19 -206
- package/dist/adapters/extension/run.js +5 -3
- package/dist/adapters/extension/runtime.js +112 -20
- package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
- package/dist/adapters/mobile/devices.js +5 -1
- package/dist/adapters/mobile/performance-observer.js +9 -6
- package/dist/adapters/mobile/run.js +1 -1
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/performance/trace-kinds.js +36 -0
- package/dist/adapters/plugin-surface.js +34 -34
- package/dist/adapters/session-lock.js +23 -0
- package/dist/adapters/web-dapp/console-capture.js +2 -20
- package/dist/adapters/web-dapp/run.js +31 -25
- package/dist/adapters/web-dapp/surface.js +32 -108
- package/dist/command-contract.js +1363 -630
- package/dist/commands/core-readiness.js +1 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/launch/device.js +1 -1
- package/dist/doctor.js +5 -2
- package/dist/mm-harness-cli.js +228 -1210
- package/dist/performance-observation.js +28 -5
- package/docs/CONTRIBUTING.md +6 -4
- package/docs/cli-contract.md +1 -1
- package/docs/feature-guard.md +1 -1
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/extension/wallet/import.mjs +21 -4
- package/library/actions/extension/wallet/visible-session.mjs +32 -12
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/web-dapp/app/launch.mjs +1 -1
- package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
- package/library/actions/web-dapp/platform/hud.mjs +1 -1
- package/library/actions/web-dapp/platform/page.mjs +2 -3
- package/library/actions/web-dapp/platform/runtime.mjs +29 -178
- package/package.json +7 -6
- package/scripts/completions.sh +1 -1
- package/adapters/web-dapp/cleanup.mjs +0 -69
- package/adapters/web-dapp/inject.mjs +0 -75
- package/adapters/web-dapp/launch.mjs +0 -631
- package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
- package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
- package/adapters/web-dapp/lib/display.mjs +0 -115
- package/adapters/web-dapp/lib/hud.mjs +0 -148
- package/adapters/web-dapp/lib/policy.mjs +0 -79
- package/adapters/web-dapp/lib/processes.mjs +0 -187
- package/adapters/web-dapp/lib/readiness.mjs +0 -272
- package/adapters/web-dapp/stop.mjs +0 -38
- package/adapters/web-dapp/verify.mjs +0 -51
- package/adapters/web-dapp/wallet-host.mjs +0 -520
- package/dist/adapters/core/live-script-env.js +0 -36
- package/dist/adapters/extension/browser-cdp.js +0 -174
- package/dist/adapters/extension/performance-observer.js +0 -75
- package/dist/adapters/performance/cdp-trace.js +0 -406
- package/dist/adapters/performance/js-task-metrics.js +0 -35
- package/dist/adapters/web-dapp/doctor-checks.js +0 -33
- package/dist/adapters/web-dapp/launch.js +0 -87
- package/dist/adapters/web-dapp/overlay.js +0 -21
- package/dist/adapters/web-dapp/readiness.js +0 -10
- package/dist/adapters/web-dapp/stop.js +0 -20
- package/dist/async.js +0 -19
- package/dist/cli.js +0 -202
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { gzip } from "node:zlib";
|
|
4
5
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { resolveExtensionId } from "./adapters/extension/extension-id.js";
|
|
7
|
+
import {
|
|
8
|
+
TRACE_KINDS,
|
|
9
|
+
TRACE_MARKER_PREFIX
|
|
10
|
+
} from "./adapters/performance/trace-kinds.js";
|
|
11
|
+
import {
|
|
12
|
+
summarizeFrames,
|
|
13
|
+
summarizeJavaScriptTasks
|
|
14
|
+
} from "@farmslot/recipe-runner/runtime/cdp-trace";
|
|
9
15
|
import {
|
|
10
16
|
indexArtifactManifest,
|
|
11
17
|
readContainedJsonArtifact,
|
|
@@ -512,7 +518,24 @@ function unavailableCapture(session, window, reason) {
|
|
|
512
518
|
);
|
|
513
519
|
}
|
|
514
520
|
async function createExtensionPerformanceObserver(target, env) {
|
|
515
|
-
|
|
521
|
+
const cdpPort = extensionCdpPort(env);
|
|
522
|
+
const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
|
|
523
|
+
if (!extensionId) {
|
|
524
|
+
throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
|
|
525
|
+
}
|
|
526
|
+
const { createExtensionPerformanceBackend } = createRequire(import.meta.url)(
|
|
527
|
+
"@farmslot/adapter-web/performance-observer"
|
|
528
|
+
);
|
|
529
|
+
return createExtensionPerformanceBackend({
|
|
530
|
+
cdpPort,
|
|
531
|
+
extensionId,
|
|
532
|
+
uiPaths: ["/home.html", "/sidepanel.html"],
|
|
533
|
+
kind: TRACE_KINDS.extension,
|
|
534
|
+
platform: "extension",
|
|
535
|
+
markerPrefix: TRACE_MARKER_PREFIX,
|
|
536
|
+
connectTimeoutMs: 1e4,
|
|
537
|
+
commandTimeoutMs: 1e4
|
|
538
|
+
});
|
|
516
539
|
}
|
|
517
540
|
function extensionCdpPort(env) {
|
|
518
541
|
const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -67,9 +67,11 @@ in the registry, so a platform registered the same way is reached by the
|
|
|
67
67
|
commands that take `--adapter` with no edit to shared code
|
|
68
68
|
(`tests/fixtures/example-adapter.ts`); `status` detects the adapter from the
|
|
69
69
|
checkout.
|
|
70
|
-
The
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
The public command table (`src/command-contract.ts`) reads the `--adapter`
|
|
71
|
+
choices when a command is validated: the registered adapters, then the adapters
|
|
72
|
+
the libraries on `RECIPE_LIBRARY_PATH` and the command's `--library` entries
|
|
73
|
+
declare. A new platform or library plugin reaches `bin/mm-harness` with no edit
|
|
74
|
+
to the table.
|
|
73
75
|
|
|
74
76
|
Extend `AdapterSurface` when a platform owns the behavior; members a platform
|
|
75
77
|
does not need are optional, and Core reports headless/N/A explicitly. Put each
|
|
@@ -82,7 +84,7 @@ command-local platform ladders for surface-owned behavior:
|
|
|
82
84
|
- `run`/`call` come from `@farmslot/recipe-cli/harness` and read `run`,
|
|
83
85
|
`recording`, `diagnostics`, `observation` and `sourceFingerprint`;
|
|
84
86
|
mm-harness hands them its engine (`src/engine.ts`) and its device targeting
|
|
85
|
-
and plan steps (`src/cli.ts`);
|
|
87
|
+
and plan steps (`src/mm-harness-cli.ts`);
|
|
86
88
|
- the runner reads `actions` (manifest, bundled semantic actions, live adapter
|
|
87
89
|
scripts, `cdp.target`, the `ui.*` transport, adapter wrappers, input checks);
|
|
88
90
|
- `install`/`verify`/`cleanup` read `harness`, `agentic-runtime.json` reads
|
package/docs/cli-contract.md
CHANGED
|
@@ -94,7 +94,7 @@ These are real farm bugs. The goldens freeze the current rejections, so fixing e
|
|
|
94
94
|
|
|
95
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.
|
|
96
96
|
|
|
97
|
-
`terminal` is the plugin the `experimental-metamask-recipe-terminal` library declares (`adapters.terminal`, extends the built-in `web-dapp`), so the family loads it: set `GOLDENS_TERMINAL_LIBRARY` to a checkout of that library
|
|
97
|
+
`terminal` is the plugin the `experimental-metamask-recipe-terminal` library declares (`adapters.terminal`, extends the built-in `web-dapp`), so the family loads it: set `GOLDENS_TERMINAL_LIBRARY` to a checkout of that library. The family copies one pinned commit of it (`GOLDENS_TERMINAL_LIBRARY_REV` in `goldens-terminal.test.sh`) into its sandbox with `git archive` and puts it on `RECIPE_LIBRARY_PATH`, so the goldens don't move when the checkout does. The checkout must contain that commit (`git fetch`); moving the pin and regenerating the goldens is one change. CI has no access to the private library and skips the family (`CI` set); local runs need the variable, and without it outside CI the family fails. A release gate must set it too (proposed for the release checklist). `goldens-discovery` covers the built-in `web-dapp` in its per-adapter loop.
|
|
98
98
|
|
|
99
99
|
Outside this boundary: a successful `launch` and the `temp/recipe/runtime/terminal/` layout (`browser.pid`), the library's recipes, `run terminal.perps.<name> --plan`, and the recipe-terminal `console-guard.mjs` import paths.
|
|
100
100
|
|
package/docs/feature-guard.md
CHANGED
|
@@ -74,7 +74,7 @@ Captures are normalized (paths, versions, pids, timestamps, durations, epoch num
|
|
|
74
74
|
|
|
75
75
|
What the normaliser deliberately masks, and so cannot catch: any semver under a key ending in `version` other than the harness's own, numbers under keys ending in `Ms`/`Seconds`/`duration`/`elapsed` (so a changed timeout value), epoch-millisecond numbers, and the `volatile` paths (a new product warning during a live run). Each varies run to run on real slots; the goldens freeze the static values.
|
|
76
76
|
|
|
77
|
-
Machine rules: on macwork, headless only and no focus stealing (`TERMINAL_HEADLESS=1` is in the matrix).
|
|
77
|
+
Machine rules: on macwork, headless only and no focus stealing (`TERMINAL_HEADLESS=1` is in the matrix). A headful `run`/`call` (Extension, including its wallet popup, or a headful web-dapp) can't proceed while macwork's screen is locked: it refuses with `SESSION_LOCKED` at its runtime check (it used to stall until a `Page.captureScreenshot` timeout). Mobile and the Extension run on free macpro or mini slots, never mini-mm-3. The differential stops and cleans the slot runtime it used; reinstall the farm's overlay afterwards if the slot is handed back to the farm.
|
|
78
78
|
|
|
79
79
|
## 3. Release smoke
|
|
80
80
|
|
|
@@ -904,6 +904,16 @@ export async function runAdapter(callback) {
|
|
|
904
904
|
// Surface the failure; still tear down so a half-open WebSocket from a
|
|
905
905
|
// failed write doesn't leave the process wedged open.
|
|
906
906
|
process.stderr.write(`[core/perps] adapter failed: ${fmtError(error)}\n`);
|
|
907
|
+
// A module the checkout cannot resolve is its environment (a partial
|
|
908
|
+
// workspace build), not app logic: say so on its own line, with the
|
|
909
|
+
// build step, for the runner's environment classification to match.
|
|
910
|
+
if (error?.code === 'ERR_MODULE_NOT_FOUND' || error?.code === 'MODULE_NOT_FOUND') {
|
|
911
|
+
const root = input.context?.projectRoot ?? process.cwd();
|
|
912
|
+
const missing = error.url ?? String(error.message ?? '').split('\n')[0];
|
|
913
|
+
process.stderr.write(
|
|
914
|
+
`[core/perps] environment: cannot resolve ${missing}\n Next: cd '${root.replace(/'/gu, `'\\''`)}' && yarn build\n`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
907
917
|
}
|
|
908
918
|
} finally {
|
|
909
919
|
await disconnectController(exitCode);
|
|
@@ -100,6 +100,23 @@ async function unlockProfile(page, credentials, timeoutMs) {
|
|
|
100
100
|
await waitForSelectorToDisappear(page, SELECTORS.unlockPassword, timeoutMs);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// A locked profile is unlocked before the account proof. A fresh profile can
|
|
104
|
+
// also lock right after onboarding while the sidepanel attaches, stranding the
|
|
105
|
+
// proof on #/unlock: that relock is recovered once with the fixture password and
|
|
106
|
+
// reported as relockRecovered; a further lock fails with the route error.
|
|
107
|
+
async function ensureUnlockedSelectedAccount(page, credentials, timeoutMs) {
|
|
108
|
+
if (await hasVisibleSelector(page, SELECTORS.unlockPassword)) await unlockProfile(page, credentials, timeoutMs);
|
|
109
|
+
try {
|
|
110
|
+
const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
|
|
111
|
+
return { ...visibleSession, relockRecovered: false };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!(await hasVisibleSelector(page, SELECTORS.unlockPassword))) throw error;
|
|
114
|
+
await unlockProfile(page, credentials, timeoutMs);
|
|
115
|
+
const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
|
|
116
|
+
return { ...visibleSession, relockRecovered: true };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
async function importThroughUi(page, input, timeoutMs) {
|
|
104
121
|
const credentials = await resolveWalletImportCredentials(input);
|
|
105
122
|
const metametricsEnabled = input.node?.metametrics === true || input.node?.metametrics === 'true';
|
|
@@ -149,7 +166,7 @@ async function importThroughUi(page, input, timeoutMs) {
|
|
|
149
166
|
await waitForSelector(page, SELECTORS.complete, timeoutMs);
|
|
150
167
|
await page.click(SELECTORS.complete);
|
|
151
168
|
|
|
152
|
-
const visibleSession = await
|
|
169
|
+
const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
|
|
153
170
|
return { credentials, visibleSession, metametricsEnabled };
|
|
154
171
|
}
|
|
155
172
|
|
|
@@ -168,9 +185,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
168
185
|
if (!onboarding) {
|
|
169
186
|
const credentials = await resolveWalletImportCredentials(input);
|
|
170
187
|
const completionTriggered = await dismissVisibleOnboardingCompletion(page);
|
|
171
|
-
const
|
|
172
|
-
if (locked) await unlockProfile(page, credentials, timeoutMs);
|
|
173
|
-
const visibleSession = await ensureVisibleSelectedAccount(page, credentials.expectedAddress, timeoutMs);
|
|
188
|
+
const visibleSession = await ensureUnlockedSelectedAccount(page, credentials, timeoutMs);
|
|
174
189
|
return {
|
|
175
190
|
action: input.action,
|
|
176
191
|
method: 'ui-visible-session',
|
|
@@ -191,6 +206,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
191
206
|
selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
|
|
192
207
|
selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
|
|
193
208
|
selectionPerformed: imported.visibleSession.selectionPerformed,
|
|
209
|
+
relockRecovered: imported.visibleSession.relockRecovered,
|
|
194
210
|
completedOnboarding: true,
|
|
195
211
|
metametricsEnabled: imported.metametricsEnabled,
|
|
196
212
|
interests: [],
|
|
@@ -213,6 +229,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
213
229
|
selectedIdentitySha256: imported.visibleSession.selectedIdentitySha256,
|
|
214
230
|
selectedAddressMatched: imported.visibleSession.selectedAddressMatched,
|
|
215
231
|
selectionPerformed: imported.visibleSession.selectionPerformed,
|
|
232
|
+
relockRecovered: imported.visibleSession.relockRecovered,
|
|
216
233
|
completedOnboarding: true,
|
|
217
234
|
metametricsEnabled: imported.metametricsEnabled,
|
|
218
235
|
interests: [],
|
|
@@ -44,7 +44,7 @@ async function currentRoute(page, deadline) {
|
|
|
44
44
|
return route.replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function routeError(page, message, deadline) {
|
|
47
|
+
async function routeError(page, message, deadline, cause) {
|
|
48
48
|
let route = '<deadline-expired>';
|
|
49
49
|
if (Date.now() < deadline) {
|
|
50
50
|
try {
|
|
@@ -53,7 +53,35 @@ async function routeError(page, message, deadline) {
|
|
|
53
53
|
route = '<unavailable>';
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const detail = cause
|
|
57
|
+
? `; last input error: ${String(cause.message ?? cause).replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200)}`
|
|
58
|
+
: '';
|
|
59
|
+
return new Error(`${message} (route ${JSON.stringify(route)})${detail}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Entry can fail transiently while the account list settles after onboarding,
|
|
63
|
+
// so it is retried in place until the shared deadline; leaving the list (a
|
|
64
|
+
// lock, a redirect) fails at once with the last input error. Callers still
|
|
65
|
+
// assert the rendered result, so a retry can never pass a wrong account.
|
|
66
|
+
async function enterAccountSearch(page, value, deadline, message) {
|
|
67
|
+
let lastError;
|
|
68
|
+
while (Date.now() <= deadline) {
|
|
69
|
+
try {
|
|
70
|
+
await withinDeadline(page.setInput(ACCOUNT_SEARCH, value), deadline);
|
|
71
|
+
return;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
lastError = error;
|
|
74
|
+
}
|
|
75
|
+
let route;
|
|
76
|
+
try {
|
|
77
|
+
route = await currentRoute(page, deadline);
|
|
78
|
+
} catch {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (route !== '#/account-list') break;
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
83
|
+
}
|
|
84
|
+
throw await routeError(page, message, deadline, lastError);
|
|
57
85
|
}
|
|
58
86
|
|
|
59
87
|
async function visibleAccountMatch(page, deadline) {
|
|
@@ -140,11 +168,7 @@ async function openAccountList(page, deadline) {
|
|
|
140
168
|
|
|
141
169
|
async function proveAddressSearchControl(page, expectedAddress, deadline) {
|
|
142
170
|
const probeAddress = accountProbeAddress(expectedAddress);
|
|
143
|
-
|
|
144
|
-
await withinDeadline(page.setInput(ACCOUNT_SEARCH, probeAddress), deadline);
|
|
145
|
-
} catch {
|
|
146
|
-
throw await routeError(page, 'Visible wallet account search could not enter the alternate-address control', deadline);
|
|
147
|
-
}
|
|
171
|
+
await enterAccountSearch(page, probeAddress, deadline, 'Visible wallet account search could not enter the alternate-address control');
|
|
148
172
|
let consecutiveEmpty = 0;
|
|
149
173
|
while (Date.now() <= deadline) {
|
|
150
174
|
let observed;
|
|
@@ -166,11 +190,7 @@ async function proveAddressSearchControl(page, expectedAddress, deadline) {
|
|
|
166
190
|
|
|
167
191
|
async function queryExpectedAccount(page, expectedAddress, deadline, requireSelected) {
|
|
168
192
|
await proveAddressSearchControl(page, expectedAddress, deadline);
|
|
169
|
-
|
|
170
|
-
await withinDeadline(page.setInput(ACCOUNT_SEARCH, expectedAddress), deadline);
|
|
171
|
-
} catch {
|
|
172
|
-
throw await routeError(page, 'Visible wallet account search could not enter the derived release identity', deadline);
|
|
173
|
-
}
|
|
193
|
+
await enterAccountSearch(page, expectedAddress, deadline, 'Visible wallet account search could not enter the derived release identity');
|
|
174
194
|
let observed;
|
|
175
195
|
while (Date.now() <= deadline) {
|
|
176
196
|
try {
|
|
@@ -15,7 +15,7 @@ export const COLLECTOR_SCRIPT_NAME = 'console-tail.mjs';
|
|
|
15
15
|
export const CAPTURE_CONTROL_MARK = '[mm-harness-capture-control]';
|
|
16
16
|
|
|
17
17
|
// Same derivation as ownerMarker('console-collector', …) in
|
|
18
|
-
//
|
|
18
|
+
// @farmslot/adapter-web/web-dapp, so the web-dapp stop path finds it.
|
|
19
19
|
export function collectorOwnerMarker(runtimeDir) {
|
|
20
20
|
const id = createHash('sha256').update(path.resolve(runtimeDir)).digest('hex').slice(0, 32);
|
|
21
21
|
return `--mm-harness-owner=console-collector-${id}`;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// start forgets the app's stored agent key and the extension's dapp permission.
|
|
5
5
|
|
|
6
6
|
import { appPort, cdpPort, isDirectRun, resolveSigner, runAdapter, webDappPolicy } from '../platform/runtime.mjs';
|
|
7
|
-
import { launchWebDappBrowser } from '
|
|
7
|
+
import { launchWebDappBrowser } from '@farmslot/adapter-web/web-dapp';
|
|
8
8
|
|
|
9
9
|
export async function launch(input) {
|
|
10
10
|
const node = input.node ?? {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Helpers from @farmslot/adapter-web's web-dapp lib that its public
|
|
2
|
+
// `./web-dapp` entry does not export, loaded from the installed package, never
|
|
3
|
+
// copied: the display and HUD helpers the actions share with the launcher and
|
|
4
|
+
// wallet host (one hud.json format), and the fixture and readiness helpers the
|
|
5
|
+
// MetaMask signer module uses. The signer module cannot import `./web-dapp`
|
|
6
|
+
// itself: the launch leaf loads it while that entry is still evaluating.
|
|
7
|
+
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const packageRoot = path.dirname(createRequire(import.meta.url).resolve('@farmslot/adapter-web/package.json'));
|
|
13
|
+
const lib = (name) => import(pathToFileURL(path.join(packageRoot, 'src/web-dapp/lib', name)).href);
|
|
14
|
+
|
|
15
|
+
export const { resolveHeadless, resolveSlowMo } = await lib('display.mjs');
|
|
16
|
+
export const { HUD_ELEMENT, clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } = await lib('hud.mjs');
|
|
17
|
+
export const { fixtureEntry } = await lib('runtime.mjs');
|
|
18
|
+
export const { check: readinessCheck } = await lib('readiness.mjs');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { isAppUrl } from '@farmslot/adapter-web/origin';
|
|
7
7
|
|
|
8
|
-
import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from '
|
|
8
|
+
import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from './adapter-web-lib.mjs';
|
|
9
9
|
import { appOrigin, cdpPort, readBrowserState, webDappRuntimeDir } from './runtime.mjs';
|
|
10
10
|
import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
|
|
11
11
|
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
// so it stays the window's active tab and keeps painting without it.
|
|
8
8
|
|
|
9
9
|
import { isAppUrl } from '@farmslot/adapter-web/origin';
|
|
10
|
+
import { cdpListenerPids } from '@farmslot/adapter-web/web-dapp';
|
|
10
11
|
|
|
11
|
-
import { resolveSlowMo } from '
|
|
12
|
-
import { HUD_ELEMENT } from '../../../../adapters/web-dapp/lib/hud.mjs';
|
|
13
|
-
import { cdpListenerPids } from '../../../../adapters/web-dapp/lib/processes.mjs';
|
|
12
|
+
import { HUD_ELEMENT, resolveSlowMo } from './adapter-web-lib.mjs';
|
|
14
13
|
import { appOrigin, cdpPort, readBrowserState } from './runtime.mjs';
|
|
15
14
|
import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
|
|
16
15
|
|
|
@@ -1,186 +1,37 @@
|
|
|
1
1
|
// Resolve the web-dapp slot runtime: the venue policy, ports, runtime paths,
|
|
2
|
-
// browser state and wallet-fixture accounts
|
|
3
|
-
//
|
|
2
|
+
// browser state and wallet-fixture accounts, from @farmslot/adapter-web; plus
|
|
3
|
+
// the live-action plumbing (input, direct run, output). Key material is only
|
|
4
|
+
// ever returned as a viem account object; callers must never log or persist it.
|
|
4
5
|
|
|
5
|
-
import { realpathSync } from 'node:fs';
|
|
6
6
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
7
|
import { pathToFileURL } from 'node:url';
|
|
9
8
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function webDappRuntimePath(projectRoot, ...segments) {
|
|
38
|
-
return path.join(webDappRuntimeDir(projectRoot), ...segments);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function portFrom(raw, label) {
|
|
42
|
-
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
43
|
-
const port = Number(raw);
|
|
44
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
45
|
-
throw new Error(`${label} must be a TCP port, got ${JSON.stringify(raw)}.`);
|
|
46
|
-
}
|
|
47
|
-
return port;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// The slot's Next.js dev server. Farmslot passes it as --watcher-port {{port}};
|
|
51
|
-
// TERMINAL_APP_PORT is the explicit override.
|
|
52
|
-
export function appPort(input = {}, env = process.env) {
|
|
53
|
-
const port =
|
|
54
|
-
portFrom(input.node?.app_port, 'app_port') ??
|
|
55
|
-
portFrom(env.TERMINAL_APP_PORT, 'TERMINAL_APP_PORT') ??
|
|
56
|
-
portFrom(env.RECIPE_WATCHER_PORT, 'RECIPE_WATCHER_PORT') ??
|
|
57
|
-
portFrom(env.WATCHER_PORT, 'WATCHER_PORT');
|
|
58
|
-
if (!port) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`${webDappPolicy().adapterId} adapter requires the slot dev-server port.\n` +
|
|
61
|
-
'Next: pass --watcher-port <port> (Farmslot: {{port}}) or set TERMINAL_APP_PORT.',
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
return port;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function appOrigin(input = {}, env = process.env) {
|
|
68
|
-
return `http://localhost:${appPort(input, env)}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function cdpPort(input = {}, env = process.env) {
|
|
72
|
-
const port =
|
|
73
|
-
portFrom(input.node?.cdp_port, 'cdp_port') ??
|
|
74
|
-
portFrom(env.RECIPE_CDP_PORT, 'RECIPE_CDP_PORT') ??
|
|
75
|
-
portFrom(env.CDP_PORT, 'CDP_PORT');
|
|
76
|
-
if (!port) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`${webDappPolicy().adapterId} adapter requires a per-slot browser CDP port.\n` +
|
|
79
|
-
'Next: pass --cdp-port <port> (Farmslot: {{cdp_port}}).',
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
return port;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function readJsonFile(file) {
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(await readFile(file, 'utf8'));
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (error?.code === 'ENOENT') return null;
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function writePrivateJson(file, value) {
|
|
95
|
-
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function readBrowserState(projectRoot) {
|
|
99
|
-
return readJsonFile(webDappRuntimePath(projectRoot, 'browser.json'));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function resolveSigner(raw) {
|
|
103
|
-
const signer = raw == null || raw === '' ? 'extension' : String(raw);
|
|
104
|
-
if (!SIGNER_MODES.includes(signer)) {
|
|
105
|
-
throw new Error(`signer must be one of ${SIGNER_MODES.join(' | ')}, got ${JSON.stringify(raw)}.`);
|
|
106
|
-
}
|
|
107
|
-
return signer;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Account name precedence: node.account_name → node.account (when not an
|
|
111
|
-
// address) → the running browser's account → dev1.
|
|
112
|
-
export function accountName(input = {}, browserState = null) {
|
|
113
|
-
const explicit = input.node?.account_name ?? input.node?.account;
|
|
114
|
-
if (typeof explicit === 'string' && explicit.trim() && !/^0x[0-9a-fA-F]{40}$/u.test(explicit.trim())) {
|
|
115
|
-
return explicit.trim();
|
|
116
|
-
}
|
|
117
|
-
return browserState?.account?.name ?? DEFAULT_ACCOUNT;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function fixturePath(projectRoot, env = process.env) {
|
|
121
|
-
return env.RECIPE_WALLET_FIXTURE ? path.resolve(env.RECIPE_WALLET_FIXTURE) : walletFixturePath(projectRoot);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function loadWalletFixture(projectRoot, env = process.env) {
|
|
125
|
-
const file = fixturePath(projectRoot, env);
|
|
126
|
-
const fixture = await readJsonFile(file);
|
|
127
|
-
if (!fixture) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`wallet fixture not found at ${file}.\n` +
|
|
130
|
-
`Next: mm-harness install --adapter ${webDappPolicy().adapterId} --target <checkout> with RECIPE_WALLET_FIXTURE set, or copy the fixture there (0600).`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
|
|
134
|
-
throw new Error(`wallet fixture at ${file} has no accounts.`);
|
|
135
|
-
}
|
|
136
|
-
return { file, fixture };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function fixtureEntry(fixture, name) {
|
|
140
|
-
const entry = fixture.accounts.find((account) => account?.name === name);
|
|
141
|
-
if (!entry) {
|
|
142
|
-
const names = fixture.accounts.map((account) => account?.name).filter(Boolean).join(', ');
|
|
143
|
-
throw new Error(`wallet fixture has no account named "${name}". Available: ${names}.`);
|
|
144
|
-
}
|
|
145
|
-
if (entry.type !== 'mnemonic' && entry.type !== 'privateKey') {
|
|
146
|
-
throw new Error(`wallet fixture account "${name}" must be mnemonic or privateKey, got "${entry.type}".`);
|
|
147
|
-
}
|
|
148
|
-
if (typeof entry.value !== 'string' || !entry.value.trim()) {
|
|
149
|
-
throw new Error(`wallet fixture account "${name}" has no value.`);
|
|
150
|
-
}
|
|
151
|
-
return entry;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Mnemonics use BIP-44 index 0, the MetaMask default and the derivation the
|
|
155
|
-
// Extension fixture flow imports.
|
|
156
|
-
export function viemAccountFromEntry(entry) {
|
|
157
|
-
if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex: 0 });
|
|
158
|
-
const raw = entry.value.trim();
|
|
159
|
-
const key = raw.startsWith('0x') ? raw : `0x${raw}`;
|
|
160
|
-
if (!/^0x[0-9a-fA-F]{64}$/u.test(key)) {
|
|
161
|
-
throw new Error(`wallet fixture privateKey account "${entry.name}" is not a 32-byte hex key.`);
|
|
162
|
-
}
|
|
163
|
-
return privateKeyToAccount(key);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function fixtureAccount(projectRoot, name, env = process.env) {
|
|
167
|
-
const { fixture } = await loadWalletFixture(projectRoot, env);
|
|
168
|
-
return viemAccountFromEntry(fixtureEntry(fixture, name));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Address-only view of the account a node targets: an explicit address wins
|
|
172
|
-
// (read-only use), otherwise the fixture account is derived.
|
|
173
|
-
export async function resolveAccountAddress(input, env = process.env) {
|
|
174
|
-
const raw = input.node?.account;
|
|
175
|
-
if (typeof raw === 'string' && /^0x[0-9a-fA-F]{40}$/u.test(raw.trim())) {
|
|
176
|
-
return { name: null, address: raw.trim() };
|
|
177
|
-
}
|
|
178
|
-
const projectRoot = input.context?.projectRoot;
|
|
179
|
-
const browserState = projectRoot ? await readBrowserState(projectRoot) : null;
|
|
180
|
-
const name = accountName(input, browserState);
|
|
181
|
-
const account = await fixtureAccount(projectRoot, name, env);
|
|
182
|
-
return { name, address: account.address };
|
|
183
|
-
}
|
|
9
|
+
import { webDappAdapterId } from '@farmslot/adapter-web/web-dapp';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
accountName,
|
|
13
|
+
appOrigin,
|
|
14
|
+
appPort,
|
|
15
|
+
canonicalPath,
|
|
16
|
+
cdpPort,
|
|
17
|
+
DEFAULT_ACCOUNT,
|
|
18
|
+
fixtureAccount,
|
|
19
|
+
fixtureEntry,
|
|
20
|
+
fixturePath,
|
|
21
|
+
loadWalletFixture,
|
|
22
|
+
POLICY_ENV,
|
|
23
|
+
readBrowserState,
|
|
24
|
+
readJsonFile,
|
|
25
|
+
resolveAccountAddress,
|
|
26
|
+
resolveSigner,
|
|
27
|
+
SIGNER_MODES,
|
|
28
|
+
viemAccountFromEntry,
|
|
29
|
+
webDappAdapterId,
|
|
30
|
+
webDappPolicy,
|
|
31
|
+
webDappRuntimeDir,
|
|
32
|
+
webDappRuntimePath,
|
|
33
|
+
writePrivateJson,
|
|
34
|
+
} from '@farmslot/adapter-web/web-dapp';
|
|
184
35
|
|
|
185
36
|
export async function loadInput() {
|
|
186
37
|
const inputPath = process.argv[2] || process.env.METAMASK_RECIPE_ADAPTER_INPUT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.84.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/adapter-
|
|
25
|
-
"@farmslot/adapter-
|
|
26
|
-
"@farmslot/adapter-
|
|
24
|
+
"@farmslot/adapter-node": "0.1.4",
|
|
25
|
+
"@farmslot/adapter-rn": "0.19.1",
|
|
26
|
+
"@farmslot/adapter-sdk": "0.7.1",
|
|
27
|
+
"@farmslot/adapter-web": "0.6.2",
|
|
27
28
|
"@farmslot/agent-runtime": "0.17.0",
|
|
28
29
|
"@farmslot/handoff": "^0.3.1",
|
|
29
30
|
"@farmslot/protocol": "0.34.0",
|
|
30
|
-
"@farmslot/recipe-cli": "0.
|
|
31
|
-
"@farmslot/recipe-runner": "0.
|
|
31
|
+
"@farmslot/recipe-cli": "0.10.0",
|
|
32
|
+
"@farmslot/recipe-runner": "0.27.0",
|
|
32
33
|
"agent-device": "0.19.3",
|
|
33
34
|
"commander": "^12.0.0",
|
|
34
35
|
"esbuild": "0.28.1",
|
package/scripts/completions.sh
CHANGED
|
@@ -45,7 +45,7 @@ _mmh_flags_for() {
|
|
|
45
45
|
execution-template) printf '%s' "--dir --domain-dir --project-worker --project-name --package-templates --package-id --flow --run-mode --platform --domain --id --provenance --include-shadowed --no-include-shadowed --title --force --json" ;;
|
|
46
46
|
recipe-quality) printf '%s' "--input --output --recipe --task --evidence --params --adapter --library --advisor --advisor-model --advisor-stages --json" ;;
|
|
47
47
|
actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
|
|
48
|
-
call) printf '%s' "--list --domain --source --sort --
|
|
48
|
+
call) printf '%s' "--list --domain --source --sort --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
49
49
|
run) printf '%s' "--list --domain --source --sort --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
50
50
|
doctor) printf '%s' "--adapter --target --fix --json" ;;
|
|
51
51
|
install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// cleanup.mjs — `mm-harness cleanup --adapter <web-dapp adapter>`: stop the slot
|
|
3
|
-
// browser and wallet host, then remove the slot's runtime state and the installed
|
|
4
|
-
// wallet fixture. Browser profiles are kept unless --reset-profile.
|
|
5
|
-
//
|
|
6
|
-
// Inputs: --target <checkout> [--cdp-port <port>] [--reset-profile]
|
|
7
|
-
// Outputs: JSON summary on stdout. Exit 0 cleaned; 1 the CDP port stayed busy.
|
|
8
|
-
// Never touches: product source files, the Next.js dev server.
|
|
9
|
-
|
|
10
|
-
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
|
|
14
|
-
import { recipeHarnessPath } from '@farmslot/recipe-cli/harness';
|
|
15
|
-
import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
|
|
16
|
-
import { webDappPolicy, webDappRuntimeDir } from '../../library/actions/web-dapp/platform/runtime.mjs';
|
|
17
|
-
import { stopConsoleCollector, stopWebDappBrowser } from './lib/processes.mjs';
|
|
18
|
-
|
|
19
|
-
const usage = 'Usage: cleanup.mjs --target <checkout> [--cdp-port <port>] [--reset-profile]';
|
|
20
|
-
|
|
21
|
-
export async function cleanupWebDappRuntime({ target, cdpPort, resetProfile = false }) {
|
|
22
|
-
const root = path.resolve(target);
|
|
23
|
-
const stop = await stopWebDappBrowser(root, { cdpPort });
|
|
24
|
-
await stopConsoleCollector(root);
|
|
25
|
-
const runtime = webDappRuntimeDir(root);
|
|
26
|
-
const removed = [];
|
|
27
|
-
if (existsSync(runtime)) {
|
|
28
|
-
for (const entry of readdirSync(runtime)) {
|
|
29
|
-
if (entry.startsWith('profile-') && !resetProfile) continue;
|
|
30
|
-
rmSync(path.join(runtime, entry), { recursive: true, force: true });
|
|
31
|
-
removed.push(entry);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const overlay = recipeHarnessPath(root, webDappPolicy().adapterId);
|
|
35
|
-
if (existsSync(overlay)) {
|
|
36
|
-
rmSync(overlay, { recursive: true, force: true });
|
|
37
|
-
removed.push(path.relative(root, overlay));
|
|
38
|
-
}
|
|
39
|
-
const fixture = walletFixturePath(root);
|
|
40
|
-
if (existsSync(fixture)) {
|
|
41
|
-
rmSync(fixture, { force: true });
|
|
42
|
-
removed.push(path.relative(root, fixture));
|
|
43
|
-
}
|
|
44
|
-
return { status: 'pass', target: root, stopped: stop.stopped, cdpPortFree: stop.cdpPortFree, removed };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
48
|
-
const argv = process.argv.slice(2);
|
|
49
|
-
if (argv.includes('--help') || argv.includes('-h')) {
|
|
50
|
-
process.stdout.write(`${usage}\n`);
|
|
51
|
-
process.exit(0);
|
|
52
|
-
}
|
|
53
|
-
const value = (flag) => {
|
|
54
|
-
const index = argv.indexOf(flag);
|
|
55
|
-
return index >= 0 ? argv[index + 1] : undefined;
|
|
56
|
-
};
|
|
57
|
-
const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
58
|
-
try {
|
|
59
|
-
const result = await cleanupWebDappRuntime({
|
|
60
|
-
target: value('--target') ?? process.cwd(),
|
|
61
|
-
cdpPort: rawPort ? Number(rawPort) : undefined,
|
|
62
|
-
resetProfile: argv.includes('--reset-profile'),
|
|
63
|
-
});
|
|
64
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
process.stdout.write(`${JSON.stringify({ status: 'fail', error: error.message })}\n`);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
}
|