@deeeed/metamask-harness 0.82.0 → 0.83.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 (34) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/adapters/core/cleanup.sh +5 -29
  3. package/adapters/core/controller-import-probe.mjs +26 -0
  4. package/adapters/manifest.json +16 -0
  5. package/adapters/web-dapp/lib/policy-fence.mjs +119 -0
  6. package/adapters/web-dapp/lib/policy.mjs +7 -2
  7. package/dist/adapters/core/actions.js +0 -3
  8. package/dist/adapters/core/overlay.js +1 -5
  9. package/dist/adapters/core/readiness.js +133 -2
  10. package/dist/adapters/core/surface.js +53 -41
  11. package/dist/adapters/extension/extension-id.js +1 -0
  12. package/dist/adapters/extension/fixtures.js +60 -1
  13. package/dist/adapters/extension/network-observer.js +19 -206
  14. package/dist/adapters/extension/run.js +1 -1
  15. package/dist/adapters/extension/runtime.js +42 -3
  16. package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
  17. package/dist/adapters/mobile/performance-observer.js +9 -6
  18. package/dist/adapters/performance/trace-kinds.js +36 -0
  19. package/dist/adapters/plugin-surface.js +29 -1
  20. package/dist/adapters/web-dapp/run.js +6 -1
  21. package/dist/command-contract.js +34 -0
  22. package/dist/commands/check.js +3 -2
  23. package/dist/commands/core-readiness.js +1 -9
  24. package/dist/performance-observation.js +28 -5
  25. package/library/actions/core/perps/_controller.mjs +10 -0
  26. package/library/actions/extension/wallet/import.mjs +21 -4
  27. package/library/actions/extension/wallet/visible-session.mjs +32 -12
  28. package/package.json +7 -6
  29. package/dist/adapters/core/live-script-env.js +0 -36
  30. package/dist/adapters/extension/browser-cdp.js +0 -174
  31. package/dist/adapters/extension/performance-observer.js +0 -75
  32. package/dist/adapters/performance/cdp-trace.js +0 -406
  33. package/dist/adapters/performance/js-task-metrics.js +0 -35
  34. package/dist/async.js +0 -19
@@ -1,10 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { colorHumanMessage, EXIT, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
3
+ import { colorHumanMessage, EXIT, shellQuote, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
4
4
  import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
5
5
  import { sameFeatureFlags } from "../../feature-flags.js";
6
6
  import { runnerDir } from "../../paths.js";
7
7
  import { readExtensionPinnedFlags } from "./feature-flags.js";
8
+ import { releaseArtifactState } from "./runtime-decision.js";
8
9
  async function applyExtensionFixture({
9
10
  target,
10
11
  fixturePath,
@@ -13,6 +14,64 @@ async function applyExtensionFixture({
13
14
  sub: action,
14
15
  fixtureFlags
15
16
  }) {
17
+ const artifact = releaseArtifactState(target);
18
+ if (artifact.status !== "none") {
19
+ const targetArg = `--target ${shellQuote(target)}`;
20
+ const cdpArg = process.env.CDP_PORT ? ` --cdp-port ${shellQuote(process.env.CDP_PORT)}` : "";
21
+ const canonical = path.resolve(fixturePath) === path.resolve(canonicalFixture);
22
+ const fixtureArg = canonical ? "" : ` --fixture ${shellQuote(fixturePath)}`;
23
+ const checkoutAction = `mm-harness runtime-launch --adapter extension ${targetArg}${cdpArg} && mm-harness fixtures ${action} --adapter extension ${targetArg}${fixtureArg}`;
24
+ let message2;
25
+ let artifactAction;
26
+ let alternatives = [];
27
+ if (artifact.status === "invalid") {
28
+ message2 = `The selected release artifact is no longer valid (${artifact.reason ?? "unknown reason"}) and fixtures will not replace it with the checkout build; re-select it with runtime-launch and the same artifact source, or seed the checkout build instead.`;
29
+ artifactAction = checkoutAction;
30
+ } else {
31
+ let archive;
32
+ try {
33
+ const provenance = JSON.parse(fs.readFileSync(artifact.provenancePath, "utf8"));
34
+ if (typeof provenance.source?.path === "string") archive = provenance.source.path;
35
+ } catch {
36
+ archive = void 0;
37
+ }
38
+ const archivePresent = archive !== void 0 && fs.existsSync(archive);
39
+ const reselect = [
40
+ `mm-harness runtime-launch --adapter extension ${targetArg}${cdpArg}`,
41
+ `--artifact-file ${archivePresent ? shellQuote(archive) : "<path-to-the-artifact-zip>"}`,
42
+ `--artifact-version ${shellQuote(artifact.expectedVersion)}`,
43
+ `--artifact-sha256 ${shellQuote(artifact.sha256)}`,
44
+ `--artifact-cache ${shellQuote(path.dirname(path.dirname(path.dirname(artifact.provenancePath))))}`,
45
+ `--chrome-user-data-dir ${shellQuote(artifact.chromeUserDataDir)}`,
46
+ "--reset-profile"
47
+ ].join(" ");
48
+ const publish = canonical ? "" : `mm-harness fixtures init --from ${shellQuote(fixturePath)} --force ${targetArg} && `;
49
+ const importWallet = `mm-harness run wallet.import method=ui credential_source=fixture --adapter extension ${targetArg}`;
50
+ artifactAction = `${publish}${reselect} && ${importWallet}`;
51
+ alternatives = [checkoutAction];
52
+ const archiveNote = archivePresent ? "" : `, supplying its archive with --artifact-file (${archive ? `${archive} no longer exists` : "its provenance names no archive"})`;
53
+ const fixtureNote = canonical ? "" : `, after the selected fixture replaces this slot's canonical fixture ${canonicalFixture}, the only one wallet.import reads`;
54
+ message2 = `Release artifact ${artifact.manifestVersion} is selected and fixtures can only seed the checkout build; re-select it with a clean profile and import the fixture wallet${archiveNote}${fixtureNote}, or seed the checkout build instead.`;
55
+ }
56
+ if (json) {
57
+ console.log(JSON.stringify({
58
+ schemaVersion: 1,
59
+ command: "fixtures",
60
+ action,
61
+ adapter: "extension",
62
+ fixture: fixturePath,
63
+ canonicalFixture,
64
+ status: "fail",
65
+ exitCode: EXIT.runtime,
66
+ error: { code: "FIXTURES_ARTIFACT_ACTIVE", message: message2, userAction: artifactAction, alternatives }
67
+ }, null, 2));
68
+ } else {
69
+ console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
70
+ Next: ${artifactAction}${alternatives.map((alternative) => `
71
+ Or: ${alternative}`).join("")}`);
72
+ }
73
+ return EXIT.runtime;
74
+ }
16
75
  const distManifest = path.join(target, "dist/chrome/manifest.json");
17
76
  const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
18
77
  if (!fs.existsSync(distManifest)) {
@@ -1,211 +1,24 @@
1
+ import { createRequire } from "node:module";
1
2
  import path from "node:path";
2
- import brokerModule from "@farmslot/recipe-runner/cdp-broker";
3
- import {
4
- asBrowserCdpTarget,
5
- createBrowserCdpClient,
6
- extensionIdFromCdpTargets,
7
- fetchCdpJson
8
- } from "./browser-cdp.js";
9
- const DEVICE_ID = "extension";
10
- const COMMAND_TIMEOUT_MS = 1e4;
11
- const EXTENSION_TARGET_TYPES = /* @__PURE__ */ new Set([
12
- "background_page",
13
- "other",
14
- "page",
15
- "service_worker"
16
- ]);
17
- const { brokerSocketPath, createBrokerClient, createCdpBroker } = brokerModule;
18
- async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
19
- if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
20
- throw new Error("Extension network observation requires a valid CDP port.");
21
- }
22
- const targets = await fetchCdpJson(cdpPort);
23
- const extensionId = extensionIdFromCdpTargets(targets);
24
- if (!extensionId) {
25
- throw new Error(
26
- "Extension CDP browser or loaded extension target is unavailable."
27
- );
28
- }
29
- const connection = await createBrowserCdpClient(cdpPort);
30
- let closed = false;
31
- const brokerRef = {};
32
- const targetSessions = /* @__PURE__ */ new Map();
33
- const attaching = /* @__PURE__ */ new Set();
34
- const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) => connection.send(method, params, sessionId, timeoutMs);
35
- const relevantTarget = (target) => {
36
- if (!EXTENSION_TARGET_TYPES.has(String(target.type))) return false;
37
- try {
38
- return new URL(String(target.url)).hostname === extensionId;
39
- } catch {
40
- return false;
41
- }
42
- };
43
- const refreshBrokerSession = () => {
44
- if (!brokerRef.current) return;
45
- brokerRef.current.onSessionClose(DEVICE_ID);
46
- brokerRef.current.onSessionOpen(DEVICE_ID);
47
- };
48
- const attachTarget = async (target) => {
49
- if (closed || !relevantTarget(target) || targetSessions.has(target.targetId) || attaching.has(target.targetId)) {
50
- return;
51
- }
52
- attaching.add(target.targetId);
53
- try {
54
- const result = asRecord(
55
- await send("Target.attachToTarget", {
56
- targetId: target.targetId,
57
- flatten: true
58
- })
59
- );
60
- const sessionId = String(result.sessionId ?? "");
61
- if (sessionId) {
62
- targetSessions.set(target.targetId, sessionId);
63
- refreshBrokerSession();
64
- }
65
- } catch (error) {
66
- if (!/already attached/iu.test(String(error))) {
67
- brokerRef.current?.onSessionClose(DEVICE_ID);
68
- }
69
- } finally {
70
- attaching.delete(target.targetId);
71
- }
72
- };
73
- connection.onMessage((message) => {
74
- if (message.method === "Target.targetCreated") {
75
- const target = asBrowserCdpTarget(message.params?.targetInfo);
76
- if (target) void attachTarget(target);
77
- return;
78
- }
79
- if (message.method === "Target.attachedToTarget") {
80
- const target = asBrowserCdpTarget(message.params?.targetInfo);
81
- const sessionId = String(message.params?.sessionId ?? "");
82
- if (target && sessionId && relevantTarget(target)) {
83
- targetSessions.set(target.targetId, sessionId);
84
- refreshBrokerSession();
85
- }
86
- return;
87
- }
88
- if (message.method === "Target.detachedFromTarget") {
89
- const sessionId = String(message.params?.sessionId ?? "");
90
- for (const [targetId, activeSessionId] of targetSessions) {
91
- if (activeSessionId !== sessionId) continue;
92
- targetSessions.delete(targetId);
93
- refreshBrokerSession();
94
- }
95
- return;
96
- }
97
- if (message.sessionId && message.method?.startsWith("Network.")) {
98
- brokerRef.current?.onCdpEvent(
99
- DEVICE_ID,
100
- message.method,
101
- message.params ?? {}
102
- );
103
- }
104
- });
105
- connection.onClose(() => {
106
- if (!closed) brokerRef.current?.onSessionClose(DEVICE_ID);
107
- });
108
- await send("Target.setDiscoverTargets", { discover: true });
109
- const targetResult = asRecord(await send("Target.getTargets"));
110
- const targetInfos = Array.isArray(targetResult.targetInfos) ? targetResult.targetInfos : [];
111
- await Promise.all(
112
- targetInfos.map(asBrowserCdpTarget).filter((target) => Boolean(target)).map(attachTarget)
113
- );
114
- if (targetSessions.size === 0) {
115
- connection.close();
116
- throw new Error("Extension CDP exposes no attachable extension targets.");
117
- }
118
- const sessions = /* @__PURE__ */ new Map([[DEVICE_ID, { brokerReady: true }]]);
119
- const socketPath = brokerSocketPath(
120
- path.join(runtimeDir, "extension-network")
121
- );
122
- brokerRef.current = createCdpBroker({
123
- socketPath,
124
- sessions,
125
- async sendCommand(_session, method, params, timeoutMs) {
126
- const sessionIds = [...new Set(targetSessions.values())];
127
- if (sessionIds.length === 0) {
128
- throw new Error(
129
- "Extension CDP has no active extension target sessions."
130
- );
131
- }
132
- const results = await Promise.allSettled(
133
- sessionIds.map(
134
- (sessionId) => send(method, params, sessionId, timeoutMs)
135
- )
136
- );
137
- const successes = results.filter(
138
- (result) => result.status === "fulfilled"
139
- );
140
- if (successes.length === 0) {
141
- const firstFailure = results.find(
142
- (result) => result.status === "rejected"
143
- );
144
- throw new Error(
145
- `Extension CDP command failed on every target: ${String(firstFailure?.reason ?? method)}`
146
- );
147
- }
148
- if (successes.length !== results.length) {
149
- results.forEach((result, index) => {
150
- if (result.status === "fulfilled") return;
151
- const failedSession = sessionIds[index];
152
- for (const [targetId, sessionId] of targetSessions) {
153
- if (sessionId === failedSession) targetSessions.delete(targetId);
154
- }
155
- });
156
- brokerRef.current?.onSessionClose(DEVICE_ID);
157
- }
158
- return successes[0].value;
159
- },
160
- requestDiscovery() {
161
- void send("Target.getTargets").then((result) => {
162
- const infos = asRecord(result).targetInfos;
163
- if (!Array.isArray(infos)) return;
164
- for (const value of infos) {
165
- const target = asBrowserCdpTarget(value);
166
- if (target) void attachTarget(target);
167
- }
168
- }).catch(() => brokerRef.current?.onSessionClose(DEVICE_ID));
169
- }
170
- });
171
- brokerRef.current.onSessionOpen(DEVICE_ID);
172
- await waitForSocket(socketPath);
173
- const client = await createBrokerClient(
174
- socketPath,
175
- DEVICE_ID,
176
- COMMAND_TIMEOUT_MS
177
- );
178
- return {
179
- start(params) {
180
- return client.control("capture-start", params, COMMAND_TIMEOUT_MS);
181
- },
182
- async end(id) {
183
- return asRecord(
184
- await client.control("capture-end", { id }, COMMAND_TIMEOUT_MS)
185
- );
186
- },
187
- async close() {
188
- closed = true;
189
- client.close();
190
- brokerRef.current?.close();
191
- connection.close();
192
- }
193
- };
194
- }
195
- function asRecord(value) {
196
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3
+ import { idFromManifest, resolveExtensionId } from "./extension-id.js";
4
+ async function createExtensionNetworkBackend(target, env, artifactsDir) {
5
+ const cdpPort = extensionCdpPort(env);
6
+ const { extensionId } = await resolveExtensionId(target, { cdpPort });
7
+ return observeExtensionNetwork(cdpPort, artifactsDir, extensionId);
197
8
  }
198
- async function waitForSocket(socketPath) {
199
- const { existsSync } = await import("node:fs");
200
- const deadline = Date.now() + COMMAND_TIMEOUT_MS;
201
- while (Date.now() < deadline) {
202
- if (existsSync(socketPath)) return;
203
- await new Promise((resolve) => setTimeout(resolve, 25));
204
- }
205
- throw new Error("Extension network observer broker did not start.");
9
+ async function createLoadedExtensionNetworkBackend(extensionDir, env, artifactsDir) {
10
+ const extensionId = idFromManifest(path.join(extensionDir, "manifest.json"));
11
+ return observeExtensionNetwork(extensionCdpPort(env), artifactsDir, extensionId);
206
12
  }
207
- async function createExtensionNetworkBackend(env, artifactsDir) {
208
- return createExtensionNetworkObserver(extensionCdpPort(env), artifactsDir);
13
+ function observeExtensionNetwork(cdpPort, artifactsDir, extensionId) {
14
+ const { createExtensionNetworkObserver } = createRequire(import.meta.url)(
15
+ "@farmslot/adapter-web/network-observer"
16
+ );
17
+ return createExtensionNetworkObserver({
18
+ cdpPort,
19
+ runtimeDir: artifactsDir,
20
+ ...extensionId ? { extensionId } : {}
21
+ });
209
22
  }
210
23
  function extensionCdpPort(env) {
211
24
  const port = Number(
@@ -218,5 +31,5 @@ function extensionCdpPort(env) {
218
31
  }
219
32
  export {
220
33
  createExtensionNetworkBackend,
221
- createExtensionNetworkObserver
34
+ createLoadedExtensionNetworkBackend
222
35
  };
@@ -181,7 +181,7 @@ const extensionDiagnostics = {
181
181
  };
182
182
  const extensionObservation = {
183
183
  network: {
184
- backend: async (_target, env, artifactsDir) => (await import("./network-observer.js")).createExtensionNetworkBackend(env, artifactsDir),
184
+ backend: async (target, env, artifactsDir) => (await import("./network-observer.js")).createExtensionNetworkBackend(target, env, artifactsDir),
185
185
  actions: true
186
186
  },
187
187
  performance: {
@@ -40,22 +40,39 @@ async function prepareExtensionRuntime(options) {
40
40
  projectRoot,
41
41
  cdpPort,
42
42
  timeoutMs: options.healthTimeoutMs,
43
- pageMode: options.pageMode
43
+ pageMode: options.pageMode,
44
+ // The validation launch seeds the copied wallet fixture into a fresh
45
+ // profile; attaching to an existing runtime may legitimately be onboarding.
46
+ seededLaunchDir: seededLaunchDir(projectRoot, launch)
44
47
  });
45
48
  return { launch, health };
46
49
  }
50
+ function seededLaunchDir(projectRoot, launch) {
51
+ return launch?.runtimeDir ? path.join(projectRoot, launch.runtimeDir, "live") : void 0;
52
+ }
53
+ const FIXTURE_SEED_RECHECK_MS = 5e3;
47
54
  async function assertHealthyExtensionRuntime(options) {
48
55
  const timeoutMs = options.timeoutMs ?? 3e4;
49
56
  const deadline = Date.now() + timeoutMs;
50
57
  let lastReport = null;
58
+ let onboardingSince;
51
59
  while (Date.now() <= deadline) {
52
60
  lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort, {
53
- pageMode: options.pageMode
61
+ pageMode: options.pageMode,
62
+ seededLaunchDir: options.seededLaunchDir
54
63
  });
55
64
  if (lastReport.status === "PASS") return lastReport;
56
65
  if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
57
66
  throw new Error(formatHealthFailure(lastReport, options.projectRoot));
58
67
  }
68
+ if (lastReport.errorCode === "FIXTURE_SEED_NOT_APPLIED") {
69
+ onboardingSince ??= Date.now();
70
+ if (Date.now() - onboardingSince >= FIXTURE_SEED_RECHECK_MS) {
71
+ throw new Error(formatHealthFailure(lastReport, options.projectRoot));
72
+ }
73
+ } else {
74
+ onboardingSince = void 0;
75
+ }
59
76
  await sleep(500);
60
77
  }
61
78
  const report = lastReport ?? failReport(options.cdpPort, ["CDP runtime was not probed."], {});
@@ -102,6 +119,24 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
102
119
  if (String(target.url ?? "").startsWith("chrome-error://")) {
103
120
  findings.push(`Extension target resolved to chrome-error page: ${target.url}`);
104
121
  }
122
+ if (options.seededLaunchDir && String(target.url ?? "").includes("/onboarding/")) {
123
+ return {
124
+ status: "FAIL",
125
+ errorCode: "FIXTURE_SEED_NOT_APPLIED",
126
+ userAction: `Read logs/fixture-source.json and logs/fixture-account-parity.json in the newest run under ${shellQuote(options.seededLaunchDir)}, fix the slot's wallet fixture (${shellQuote(path.join(projectRoot, recipeRuntimeDir(), "wallet-fixture.json"))}), then rerun with --launch-existing-dist.`,
127
+ cdpPort,
128
+ targetUrl: target.url,
129
+ extensionId: safeExtensionId(target),
130
+ extensionPageTargets: extensionTargets.length,
131
+ findings: [
132
+ "FIXTURE_SEED_NOT_APPLIED: MetaMask is still on onboarding after launch: the wallet fixture was not applied."
133
+ ],
134
+ details: {
135
+ projectRoot,
136
+ targetUrls: targets.map((entry) => entry.url).filter(Boolean)
137
+ }
138
+ };
139
+ }
105
140
  let session;
106
141
  try {
107
142
  session = await CdpSession.connect(target.webSocketDebuggerUrl, {
@@ -653,6 +688,9 @@ async function launchLockedValidationBrowser(options) {
653
688
  "--cdp-port",
654
689
  String(options.cdpPort),
655
690
  "--launch-existing-dist",
691
+ // The profile is a fresh mkdtemp dir; live.sh resolves and seeds the copied
692
+ // wallet fixture only on a reset launch.
693
+ "--reset-profile",
656
694
  "--chrome-user-data-dir",
657
695
  validationProfilePath,
658
696
  "--artifacts-dir",
@@ -1001,5 +1039,6 @@ export {
1001
1039
  evmRpcRecoveryAction,
1002
1040
  extensionBackgroundProbeTimeoutMs,
1003
1041
  formatHealthFailure,
1004
- prepareExtensionRuntime
1042
+ prepareExtensionRuntime,
1043
+ seededLaunchDir
1005
1044
  };
@@ -1,6 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { summarizeFrames } from "@farmslot/adapter-rn";
3
+ import {
4
+ summarizeFrames
5
+ } from "@farmslot/recipe-runner/runtime/cdp-trace";
4
6
  const execFileAsync = promisify(execFile);
5
7
  const POLL_INTERVAL_MS = 750;
6
8
  const MAX_FRAMES = 1e5;
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createCdpTraceCollector
3
- } from "../performance/cdp-trace.js";
3
+ } from "@farmslot/recipe-runner/runtime/cdp-trace";
4
+ import { TRACE_KINDS, TRACE_MARKER_PREFIX } from "../performance/trace-kinds.js";
4
5
  import { connectMobileBroker } from "./network-observer.js";
5
6
  import { createAndroidGfxinfoPerformanceBackend } from "./android-gfxinfo-performance.js";
6
7
  async function createMobilePerformanceBackend(target, env) {
@@ -8,10 +9,12 @@ async function createMobilePerformanceBackend(target, env) {
8
9
  return createAndroidGfxinfoPerformanceBackend(env);
9
10
  }
10
11
  const { client } = await connectMobileBroker(target, env);
11
- return createCdpTraceCollector(
12
- client,
13
- resolvePlatform(env),
14
- async (name) => {
12
+ const platform = resolvePlatform(env);
13
+ return createCdpTraceCollector(client, {
14
+ kind: TRACE_KINDS[platform],
15
+ platform,
16
+ markerPrefix: TRACE_MARKER_PREFIX,
17
+ async marker(name) {
15
18
  const before = Date.now();
16
19
  await markRuntime(client, name);
17
20
  const after = Date.now();
@@ -20,7 +23,7 @@ async function createMobilePerformanceBackend(target, env) {
20
23
  uncertaintyMs: (after - before) / 2
21
24
  };
22
25
  }
23
- );
26
+ });
24
27
  }
25
28
  async function markRuntime(client, name) {
26
29
  await client.send(
@@ -0,0 +1,36 @@
1
+ const TRACE_MARKER_PREFIX = "mmh-clock-";
2
+ const MOBILE_TRACE_CATEGORIES = [
3
+ "blink.user_timing",
4
+ "disabled-by-default-devtools.timeline.frame"
5
+ ];
6
+ const EXTENSION_TRACE_CATEGORIES = [
7
+ "blink.user_timing",
8
+ "devtools.timeline",
9
+ "disabled-by-default-devtools.timeline",
10
+ "disabled-by-default-devtools.timeline.frame",
11
+ "v8.execute"
12
+ ];
13
+ const ANDROID_TRACE_KIND = {
14
+ categories: MOBILE_TRACE_CATEGORIES,
15
+ rendererScoped: false,
16
+ scope: "mobile-host",
17
+ nativeSource: "react-native-cdp-frame-timings",
18
+ javascriptTasks: false,
19
+ frameTiming: "draw"
20
+ };
21
+ const TRACE_KINDS = {
22
+ extension: {
23
+ categories: EXTENSION_TRACE_CATEGORIES,
24
+ rendererScoped: true,
25
+ scope: "extension-renderer",
26
+ nativeSource: "chromium-cdp-frame-timings",
27
+ javascriptTasks: true,
28
+ frameTiming: "draw"
29
+ },
30
+ ios: { ...ANDROID_TRACE_KIND, frameTiming: "cadence" },
31
+ android: ANDROID_TRACE_KIND
32
+ };
33
+ export {
34
+ TRACE_KINDS,
35
+ TRACE_MARKER_PREFIX
36
+ };
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import {
3
4
  adapterPlugin,
@@ -5,6 +6,9 @@ import {
5
6
  ensureAdapterLoaded,
6
7
  selectedAdapterId
7
8
  } from "@farmslot/recipe-cli/harness";
9
+ import { libraryAdapterFiles } from "@farmslot/recipe-runner";
10
+ import { POLICY_ENV } from "../../adapters/web-dapp/lib/policy.mjs";
11
+ import { POLICY_DIGEST_ENV, PolicyFenceError, fencePolicy } from "../../adapters/web-dapp/lib/policy-fence.mjs";
8
12
  import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
9
13
  import { adapterRegistry } from "./registry.js";
10
14
  import { bindWebDappPolicy } from "./web-dapp/surface.js";
@@ -34,6 +38,7 @@ async function loadSelectedAdapter(command, args) {
34
38
  else if (arg.startsWith("--library=")) libraries.push(arg.slice("--library=".length));
35
39
  });
36
40
  const selected = selectedAdapterId(args);
41
+ const builtin = selected !== void 0 && adapterRegistry.has(selected);
37
42
  try {
38
43
  await ensureAdapterLoaded(selected, {
39
44
  libraries,
@@ -41,11 +46,12 @@ async function loadSelectedAdapter(command, args) {
41
46
  env: operatorLibraryEnv(),
42
47
  adopt: adoptPluginAdapter
43
48
  });
49
+ await fenceBoundPolicy();
44
50
  return void 0;
45
51
  } catch (error) {
46
52
  const refusal = error;
47
53
  if (typeof refusal.code !== "string" || typeof refusal.userAction !== "string") throw error;
48
- if (selected !== void 0 && adapterRegistry.has(selected) && refusal.code !== "ADAPTER_ID_CONFLICT") {
54
+ if (builtin && refusal.code !== "ADAPTER_ID_CONFLICT") {
49
55
  return void 0;
50
56
  }
51
57
  return adapterSelectionFailureOut(own.includes("--json"), command, {
@@ -56,8 +62,30 @@ async function loadSelectedAdapter(command, args) {
56
62
  }
57
63
  }
58
64
  const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
65
+ let boundPolicy;
66
+ async function fenceBoundPolicy() {
67
+ const bound = boundPolicy;
68
+ boundPolicy = void 0;
69
+ if (!bound || process.env[POLICY_ENV] !== bound.file) return;
70
+ const plugin = adapterPlugin(bound.adapterId);
71
+ try {
72
+ if (!plugin) throw new PolicyFenceError(`no plugin record for adapter '${bound.adapterId}'`);
73
+ const files = await libraryAdapterFiles(plugin.root, { module: plugin.module });
74
+ const covered = new Set(files.map((file) => fs.realpathSync(path.join(plugin.root, file))));
75
+ const scope = `${path.posix.dirname(plugin.module.replace(/^\.\//u, ""))}/ and actions/ in ${plugin.root}`;
76
+ process.env[POLICY_DIGEST_ENV] = fencePolicy({ file: bound.file, covered, scope });
77
+ } catch (error) {
78
+ delete process.env[POLICY_ENV];
79
+ delete process.env[POLICY_DIGEST_ENV];
80
+ throw error;
81
+ }
82
+ }
59
83
  function adoptPluginAdapter(adapter) {
60
84
  const surface = bindWebDappPolicy(adapter);
85
+ const policy = adapter;
86
+ if (policy.extends === "web-dapp" && typeof policy.policy?.module === "string") {
87
+ boundPolicy = { adapterId: adapter.id, file: policy.policy.module };
88
+ }
61
89
  return {
62
90
  ...surface,
63
91
  run: {
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { isRecord } from "@farmslot/recipe-cli/harness";
2
3
  import { verifyOwnedCollectorControl } from "../console-capture.js";
3
4
  import { devicePlatformFromEnv } from "../performance/device-platform.js";
@@ -40,7 +41,11 @@ function walletRequestFindings(lines) {
40
41
  }
41
42
  const webDappObservation = {
42
43
  network: {
43
- backend: async (_target, env, artifactsDir) => (await import("../extension/network-observer.js")).createExtensionNetworkBackend(env, artifactsDir)
44
+ backend: async (target, env, artifactsDir) => (await import("../extension/network-observer.js")).createLoadedExtensionNetworkBackend(
45
+ path.resolve(env.TERMINAL_EXTENSION_DIR || path.join(target, "temp/metamask-extension/chrome")),
46
+ env,
47
+ artifactsDir
48
+ )
44
49
  },
45
50
  performance: {
46
51
  start: async (context) => {
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { adapterChoices, closest } from "@farmslot/recipe-cli/harness";
2
4
  import { configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
3
5
  const bool = () => ({ kind: "boolean" });
@@ -440,6 +442,36 @@ function optionValues(args, name) {
440
442
  }
441
443
  return values;
442
444
  }
445
+ const LIBRARY_ADAPTERS = {
446
+ terminal: { title: "MetaMask Web Terminal", repository: "MetaMask/experimental-metamask-recipe-terminal" }
447
+ };
448
+ const quoteArg = (value2) => /^[A-Za-z0-9_./:=@%+-]+$/u.test(value2) ? value2 : `'${value2.replaceAll("'", `'\\''`)}'`;
449
+ function missingAdapterLibrary(command, optionName, id, tokens) {
450
+ const library = LIBRARY_ADAPTERS[id];
451
+ if (!library || optionName !== "--adapter" && optionName !== "--platform") return void 0;
452
+ const current = operatorLibraryEnv().RECIPE_LIBRARY_PATH;
453
+ const rerun = (libraryPath) => `RECIPE_LIBRARY_PATH=${quoteArg(libraryPath)} mm-harness ${[command, ...tokens].map(quoteArg).join(" ")}`;
454
+ const revision = `a revision whose recipe-library.json declares adapters.${id}`;
455
+ const older = (current ?? "").split(":").filter(Boolean).map((entry) => {
456
+ const separator = entry.indexOf("=");
457
+ return separator > 0 && !entry.slice(0, separator).includes("/") ? { name: entry.slice(0, separator), root: entry.slice(separator + 1) } : { name: void 0, root: entry };
458
+ }).find((entry) => entry.name === id || fs.existsSync(path.join(entry.root, "recipes", id)));
459
+ if (older) {
460
+ return {
461
+ code: "CLI_ADAPTER_LIBRARY_MISSING",
462
+ command,
463
+ message: `${optionName} ${id} is the ${library.title} adapter, a plugin of the ${library.repository} recipe library. The library on RECIPE_LIBRARY_PATH at ${older.root} does not declare it: an older checkout, or a recipe-library.json that does not parse.`,
464
+ userAction: `Update that checkout to ${revision} (e.g. git -C ${quoteArg(older.root)} pull), then rerun: ${rerun(current)}`
465
+ };
466
+ }
467
+ const checkout = `<${library.repository.split("/")[1]} checkout>`;
468
+ return {
469
+ code: "CLI_ADAPTER_LIBRARY_MISSING",
470
+ command,
471
+ message: `${optionName} ${id} is the ${library.title} adapter, a plugin of the ${library.repository} recipe library, and no library on RECIPE_LIBRARY_PATH, --library or the config declares it.`,
472
+ userAction: `Keep ${optionName} ${id} and put that library, at ${revision}, on RECIPE_LIBRARY_PATH (git clone https://github.com/${library.repository}.git ${checkout}): ${rerun(current ? `${id}=${checkout}:${current}` : `${id}=${checkout}`)}`
473
+ };
474
+ }
443
475
  function validatePublicInvocation(argv, examples = {}) {
444
476
  const token = argv[0];
445
477
  if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
@@ -509,6 +541,8 @@ function validatePublicInvocation(argv, examples = {}) {
509
541
  if (inlineValue === void 0) index += 1;
510
542
  const choices = typeof spec.choices === "function" ? spec.choices(tokens) : spec.choices;
511
543
  if (choices && !choices.includes(optionValue)) {
544
+ const missing = missingAdapterLibrary(name, optionName, optionValue, tokens);
545
+ if (missing) return missing;
512
546
  const suggestion = closest(optionValue, choices);
513
547
  return {
514
548
  code: "CLI_INVALID_OPTION_VALUE",
@@ -7,6 +7,7 @@ const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter <adapt
7
7
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
8
8
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
9
9
  const TEST_FILE_RE = /(?:^|[./_-])(?:test|spec)\.(?:c|m)?jsx?$|(?:^|[./_-])(?:test|spec)\.tsx?$/u;
10
+ const OXFMT_TARGET_ARGS = ["--no-error-on-unmatched-pattern"];
10
11
  const ESLINT_SUPPRESSION_RE = /^\s*eslint-disable(?:-line|-next-line)?\b/u;
11
12
  const EXTENSION_LOCALE_RE = /^app\/_locales\//u;
12
13
  async function handleCheck(argv) {
@@ -427,7 +428,7 @@ function runFormatterCheck(input) {
427
428
  label: "Changed-file oxfmt",
428
429
  ...input,
429
430
  command: oxfmt,
430
- args: ["--config", oxfmtConfig, "--check"]
431
+ args: ["--config", oxfmtConfig, "--check", ...OXFMT_TARGET_ARGS]
431
432
  });
432
433
  }
433
434
  return runToolCheck({
@@ -447,7 +448,7 @@ function runFormatterFix(input) {
447
448
  label: "Changed-file oxfmt fix",
448
449
  ...input,
449
450
  command: oxfmt,
450
- args: ["--config", oxfmtConfig, "--write"]
451
+ args: ["--config", oxfmtConfig, "--write", ...OXFMT_TARGET_ARGS]
451
452
  });
452
453
  }
453
454
  return runToolCheck({