@deeeed/metamask-harness 0.83.0 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +8 -1
  3. package/adapters/manifest.json +14 -110
  4. package/adapters/shared/session-lock.mjs +43 -0
  5. package/adapters/web-dapp/metamask-signer.mjs +212 -0
  6. package/dist/adapters/extension/run.js +4 -2
  7. package/dist/adapters/extension/runtime.js +71 -18
  8. package/dist/adapters/mobile/devices.js +5 -1
  9. package/dist/adapters/mobile/run.js +1 -1
  10. package/dist/adapters/mobile/surface.js +1 -2
  11. package/dist/adapters/plugin-surface.js +14 -42
  12. package/dist/adapters/session-lock.js +23 -0
  13. package/dist/adapters/web-dapp/console-capture.js +2 -20
  14. package/dist/adapters/web-dapp/run.js +25 -24
  15. package/dist/adapters/web-dapp/surface.js +32 -108
  16. package/dist/command-contract.js +1342 -643
  17. package/dist/commands/doctor.js +1 -1
  18. package/dist/commands/launch/device.js +1 -1
  19. package/dist/doctor.js +5 -2
  20. package/dist/mm-harness-cli.js +228 -1210
  21. package/docs/CONTRIBUTING.md +6 -4
  22. package/docs/cli-contract.md +1 -1
  23. package/docs/feature-guard.md +1 -1
  24. package/library/actions/shared/console-collector.mjs +1 -1
  25. package/library/actions/web-dapp/app/launch.mjs +1 -1
  26. package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
  27. package/library/actions/web-dapp/platform/hud.mjs +1 -1
  28. package/library/actions/web-dapp/platform/page.mjs +2 -3
  29. package/library/actions/web-dapp/platform/runtime.mjs +29 -178
  30. package/package.json +7 -7
  31. package/scripts/completions.sh +1 -1
  32. package/adapters/web-dapp/cleanup.mjs +0 -69
  33. package/adapters/web-dapp/inject.mjs +0 -75
  34. package/adapters/web-dapp/launch.mjs +0 -631
  35. package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
  36. package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
  37. package/adapters/web-dapp/lib/display.mjs +0 -115
  38. package/adapters/web-dapp/lib/hud.mjs +0 -148
  39. package/adapters/web-dapp/lib/policy-fence.mjs +0 -119
  40. package/adapters/web-dapp/lib/policy.mjs +0 -84
  41. package/adapters/web-dapp/lib/processes.mjs +0 -187
  42. package/adapters/web-dapp/lib/readiness.mjs +0 -272
  43. package/adapters/web-dapp/stop.mjs +0 -38
  44. package/adapters/web-dapp/verify.mjs +0 -51
  45. package/adapters/web-dapp/wallet-host.mjs +0 -520
  46. package/dist/adapters/web-dapp/doctor-checks.js +0 -33
  47. package/dist/adapters/web-dapp/launch.js +0 -87
  48. package/dist/adapters/web-dapp/overlay.js +0 -21
  49. package/dist/adapters/web-dapp/readiness.js +0 -10
  50. package/dist/adapters/web-dapp/stop.js +0 -20
  51. package/dist/cli.js +0 -202
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
5
5
  import path from "node:path";
6
6
  import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, resolveFarmslotPoolDir, runnerDir } from "../../paths.js";
7
7
  import { poolFilesForHost } from "../resolve-slot-ports.js";
8
+ import { macosSessionLocked, SESSION_LOCKED_MESSAGE, SESSION_LOCKED_USER_ACTION } from "../../../adapters/shared/session-lock.mjs";
8
9
  import { profileProcessPids } from "./validation-process-ownership.js";
9
10
  const require2 = createRequire(import.meta.url);
10
11
  const { RUNTIME_NONCE_PREFIX, detachedLaunchUnprovenPath, hasDetachedLaunchUnproven, shellQuote } = require2(
@@ -62,7 +63,7 @@ async function assertHealthyExtensionRuntime(options) {
62
63
  seededLaunchDir: options.seededLaunchDir
63
64
  });
64
65
  if (lastReport.status === "PASS") return lastReport;
65
- if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
66
+ if (lastReport.errorCode === "SESSION_LOCKED" || lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
66
67
  throw new Error(formatHealthFailure(lastReport, options.projectRoot));
67
68
  }
68
69
  if (lastReport.errorCode === "FIXTURE_SEED_NOT_APPLIED") {
@@ -149,18 +150,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
149
150
  Math.min(1e3, cdpCallTimeoutMs)
150
151
  );
151
152
  if (compositor.status === "suspended") {
152
- const userAction = compositorRecoveryAction(projectRoot);
153
+ const suspended = `UI_COMPOSITOR_SUSPENDED: the Extension page is reachable over CDP but its render compositor is not advancing (${compositor.reason ?? "unknown reason"}).`;
154
+ const locked = macosSessionLocked() === true;
153
155
  return {
154
156
  status: "FAIL",
155
- errorCode: "UI_COMPOSITOR_SUSPENDED",
156
- userAction,
157
+ errorCode: locked ? "SESSION_LOCKED" : "UI_COMPOSITOR_SUSPENDED",
158
+ userAction: locked ? SESSION_LOCKED_USER_ACTION : compositorRecoveryAction(projectRoot),
157
159
  cdpPort,
158
160
  targetUrl: target.url,
159
161
  extensionId: safeExtensionId(target),
160
162
  extensionPageTargets: extensionTargets.length,
161
- findings: [
162
- `UI_COMPOSITOR_SUSPENDED: the Extension page is reachable over CDP but its render compositor is not advancing (${compositor.reason ?? "unknown reason"}).`
163
- ],
163
+ findings: locked ? [`SESSION_LOCKED: ${SESSION_LOCKED_MESSAGE}`, suspended] : [suspended],
164
164
  details: {
165
165
  projectRoot,
166
166
  targetUrls: targets.map((entry) => entry.url).filter(Boolean),
@@ -669,7 +669,7 @@ async function launchLockedValidationBrowser(options) {
669
669
  await stopOwnedCdpListener(options.cdpPort, [
670
670
  validationRuntimePath,
671
671
  path.join(options.projectRoot, recipeRuntimeDir())
672
- ]);
672
+ ], path.join(options.projectRoot, recipeRuntimeDir()));
673
673
  assertNoLiveValidationProfileOwners(validationProfileRoot);
674
674
  pruneValidationRuns(validationProfileRoot, 0);
675
675
  pruneValidationRuns(validationArtifactsRoot, 4);
@@ -933,16 +933,32 @@ function runProcess(command, args, options) {
933
933
  });
934
934
  });
935
935
  }
936
- async function stopOwnedCdpListener(cdpPort, runtimePaths) {
936
+ async function stopOwnedCdpListener(cdpPort, runtimePaths, checkoutRuntimeRoot) {
937
937
  if (process.platform !== "darwin" && process.platform !== "linux") return;
938
938
  let listenerPids = await inspectCdpListenerPids(cdpPort);
939
939
  if (listenerPids.length === 0) return;
940
940
  const identity = runtimePaths.map((runtimePath) => readValidationRuntimeIdentity(runtimePath, cdpPort)).find((candidate) => candidate?.pid === listenerPids[0]);
941
- if (!identity || listenerPids.length !== 1 || listenerPids[0] !== identity.pid || !await browserEchoesRuntimeNonce(cdpPort, identity.nonce)) {
942
- throw new Error(`Extension validation will not stop an unowned CDP listener on port ${cdpPort}.`);
941
+ let pid;
942
+ if (identity && listenerPids.length === 1 && listenerPids[0] === identity.pid && await browserEchoesRuntimeNonce(cdpPort, identity.nonce)) {
943
+ pid = identity.pid;
944
+ }
945
+ if (pid === void 0) {
946
+ const listeners = await listenerCommands(listenerPids);
947
+ const listener = listenerPids.length === 1 ? listeners.get(listenerPids[0]) : void 0;
948
+ if (listener?.chrome && !listener.ambiguous && pathInsideReal(checkoutRuntimeRoot, listener.profile) && (await listenerCommands(listenerPids)).get(listenerPids[0])?.command === listener.command) {
949
+ pid = listenerPids[0];
950
+ } else {
951
+ const seen = listenerPids.map((candidate) => {
952
+ const profile = listeners.get(candidate)?.profile;
953
+ return profile ? `pid ${candidate} --user-data-dir=${profile}` : `pid ${candidate}`;
954
+ }).join("; ");
955
+ throw new Error(
956
+ `Extension validation will not stop an unowned CDP listener on port ${cdpPort} (${seen}). Next: if that is this slot's browser, stop it (kill ${listenerPids.join(" ")}); otherwise free port ${cdpPort}. Then rerun.`
957
+ );
958
+ }
943
959
  }
944
960
  try {
945
- process.kill(identity.pid, "SIGTERM");
961
+ process.kill(pid, "SIGTERM");
946
962
  } catch (error) {
947
963
  if (error.code !== "ESRCH") throw error;
948
964
  }
@@ -950,10 +966,10 @@ async function stopOwnedCdpListener(cdpPort, runtimePaths) {
950
966
  do {
951
967
  await sleep(50);
952
968
  listenerPids = await inspectCdpListenerPids(cdpPort);
953
- } while ((listenerPids.length > 0 || processExists(identity.pid)) && Date.now() < termDeadline);
954
- if (listenerPids.length > 0 || processExists(identity.pid)) {
969
+ } while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < termDeadline);
970
+ if (listenerPids.length > 0 || processExists(pid)) {
955
971
  try {
956
- process.kill(identity.pid, "SIGKILL");
972
+ process.kill(pid, "SIGKILL");
957
973
  } catch (error) {
958
974
  if (error.code !== "ESRCH") throw error;
959
975
  }
@@ -961,12 +977,48 @@ async function stopOwnedCdpListener(cdpPort, runtimePaths) {
961
977
  do {
962
978
  await sleep(50);
963
979
  listenerPids = await inspectCdpListenerPids(cdpPort);
964
- } while ((listenerPids.length > 0 || processExists(identity.pid)) && Date.now() < killDeadline);
980
+ } while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < killDeadline);
965
981
  }
966
- if (listenerPids.length > 0 || processExists(identity.pid)) {
982
+ if (listenerPids.length > 0 || processExists(pid)) {
967
983
  throw new Error(`Extension validation could not stop its CDP listener on port ${cdpPort}.`);
968
984
  }
969
985
  }
986
+ async function listenerCommands(pids) {
987
+ const listeners = /* @__PURE__ */ new Map();
988
+ const result = await runProcess("ps", ["-ww", "-o", "pid=,command=", "-p", pids.join(",")], {
989
+ cwd: runnerDir,
990
+ env: process.env,
991
+ timeoutMs: 5e3
992
+ });
993
+ if (result.timedOut) return listeners;
994
+ for (const line of result.stdout.split("\n")) {
995
+ const match = line.match(/^\s*(\d+)\s+(.*)$/u);
996
+ if (!match) continue;
997
+ const command = match[2];
998
+ const flags = [...command.matchAll(/(?:^|\s)--user-data-dir=/gu)];
999
+ const last = flags.at(-1);
1000
+ if (!last) continue;
1001
+ const value = command.slice(last.index + last[0].length);
1002
+ const ends = [...value.matchAll(/\s--/gu)].map((end) => end.index);
1003
+ const profile = value.slice(0, ends[0] ?? value.length);
1004
+ const longerReadings = [...ends.slice(1), value.length].map((end) => value.slice(0, end));
1005
+ listeners.set(Number(match[1]), {
1006
+ command,
1007
+ profile,
1008
+ chrome: /chrom/iu.test(path.basename(command.split(/\s-/u)[0])),
1009
+ ambiguous: flags.length > 1 || !path.isAbsolute(profile) || ends.length > 0 && longerReadings.some((reading) => fs.existsSync(reading))
1010
+ });
1011
+ }
1012
+ return listeners;
1013
+ }
1014
+ function pathInsideReal(root, candidate) {
1015
+ try {
1016
+ const relative = path.relative(fs.realpathSync(root), fs.realpathSync(candidate));
1017
+ return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
1018
+ } catch {
1019
+ return false;
1020
+ }
1021
+ }
970
1022
  async function inspectCdpListenerPids(cdpPort) {
971
1023
  const result = await runProcess("lsof", ["-ti", `tcp:${cdpPort}`, "-sTCP:LISTEN"], {
972
1024
  cwd: runnerDir,
@@ -1040,5 +1092,6 @@ export {
1040
1092
  extensionBackgroundProbeTimeoutMs,
1041
1093
  formatHealthFailure,
1042
1094
  prepareExtensionRuntime,
1043
- seededLaunchDir
1095
+ seededLaunchDir,
1096
+ stopOwnedCdpListener
1044
1097
  };
@@ -9,7 +9,11 @@ import {
9
9
  } from "../../commands/mobile-device-view.js";
10
10
  const mobileDevices = {
11
11
  view: mobileDeviceView,
12
- live: mobileDeviceLiveView,
12
+ // adapter-sdk's live view names the app's feature-flag overrides `featureFlags`.
13
+ async live(target, view) {
14
+ const live = await mobileDeviceLiveView(target, view);
15
+ return { ...live, featureFlags: live.featureFlagOverrides };
16
+ },
13
17
  renderList: renderMobileDeviceList,
14
18
  renderAdditional: renderAdditionalReachableDevices,
15
19
  renderLive: renderMobileLiveBlock,
@@ -82,7 +82,7 @@ async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
82
82
  }
83
83
  if (initial.status === "current") return null;
84
84
  const targetArg = JSON.stringify(path.resolve(target));
85
- const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
85
+ const userAction = `mm-harness call app.lifecycle command=restart --adapter mobile --target ${targetArg}`;
86
86
  const message = `Mobile loaded source is not current (${initial.status}).`;
87
87
  emitProofFailure(json, {
88
88
  recoverable: true,
@@ -53,9 +53,8 @@ const mobileSurface = defineAdapter({
53
53
  devServer: {
54
54
  label: "metro",
55
55
  describe: () => "Metro dev server",
56
- // Metro's leaves read the port as METRO_PORT; run and call take it as --metro-port.
56
+ // Metro's leaves read the port as METRO_PORT.
57
57
  portEnv: ["METRO_PORT"],
58
- portFlags: ["metroPort"],
59
58
  stop(target) {
60
59
  const leaf = path.join(runnerDir, "adapters", "mobile", "stop-metro.sh");
61
60
  const args = ["--target", target];
@@ -1,16 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import {
4
- adapterPlugin,
5
- adapterSelectionFailureOut,
6
- ensureAdapterLoaded,
7
- selectedAdapterId
8
- } from "@farmslot/recipe-cli/harness";
3
+ import { fencePolicy, POLICY_DIGEST_ENV, POLICY_ENV, PolicyFenceError } from "@farmslot/adapter-web/web-dapp";
4
+ import { adapterPlugin, AdapterPluginError } from "@farmslot/recipe-cli/harness";
9
5
  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";
12
6
  import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
13
- import { adapterRegistry } from "./registry.js";
14
7
  import { bindWebDappPolicy } from "./web-dapp/surface.js";
15
8
  function configuredLibrarySources() {
16
9
  let libraries;
@@ -29,38 +22,6 @@ function operatorLibraryEnv() {
29
22
  else env.RECIPE_LIBRARY_PATH = declared.value;
30
23
  return env;
31
24
  }
32
- async function loadSelectedAdapter(command, args) {
33
- const passthrough = args.indexOf("--");
34
- const own = passthrough < 0 ? args : args.slice(0, passthrough);
35
- const libraries = [];
36
- own.forEach((arg, index) => {
37
- if (arg === "--library" && own[index + 1] !== void 0) libraries.push(own[index + 1]);
38
- else if (arg.startsWith("--library=")) libraries.push(arg.slice("--library=".length));
39
- });
40
- const selected = selectedAdapterId(args);
41
- const builtin = selected !== void 0 && adapterRegistry.has(selected);
42
- try {
43
- await ensureAdapterLoaded(selected, {
44
- libraries,
45
- configured: configuredLibrarySources(),
46
- env: operatorLibraryEnv(),
47
- adopt: adoptPluginAdapter
48
- });
49
- await fenceBoundPolicy();
50
- return void 0;
51
- } catch (error) {
52
- const refusal = error;
53
- if (typeof refusal.code !== "string" || typeof refusal.userAction !== "string") throw error;
54
- if (builtin && refusal.code !== "ADAPTER_ID_CONFLICT") {
55
- return void 0;
56
- }
57
- return adapterSelectionFailureOut(own.includes("--json"), command, {
58
- code: refusal.code,
59
- message: String(refusal.message),
60
- userAction: refusal.userAction
61
- });
62
- }
63
- }
64
25
  const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
65
26
  let boundPolicy;
66
27
  async function fenceBoundPolicy() {
@@ -80,6 +41,16 @@ async function fenceBoundPolicy() {
80
41
  throw error;
81
42
  }
82
43
  }
44
+ async function afterAdapterLoad() {
45
+ try {
46
+ await fenceBoundPolicy();
47
+ } catch (error) {
48
+ if (error instanceof PolicyFenceError) {
49
+ throw new AdapterPluginError("ADAPTER_PLUGIN_INVALID", error.message, error.userAction);
50
+ }
51
+ throw error;
52
+ }
53
+ }
83
54
  function adoptPluginAdapter(adapter) {
84
55
  const surface = bindWebDappPolicy(adapter);
85
56
  const policy = adapter;
@@ -120,7 +91,8 @@ function adoptPluginAdapter(adapter) {
120
91
  export {
121
92
  ADAPTER_PLUGIN_ENV,
122
93
  adoptPluginAdapter,
94
+ afterAdapterLoad,
123
95
  configuredLibrarySources,
124
- loadSelectedAdapter,
96
+ fenceBoundPolicy,
125
97
  operatorLibraryEnv
126
98
  };
@@ -0,0 +1,23 @@
1
+ import { Writable } from "node:stream";
2
+ import { EXIT, optionFlag } from "@farmslot/recipe-cli/harness";
3
+ import {
4
+ macosSessionLocked,
5
+ SESSION_LOCKED_MESSAGE,
6
+ SESSION_LOCKED_USER_ACTION
7
+ } from "../../adapters/shared/session-lock.mjs";
8
+ import { emitProofFailure } from "./proof-failure.js";
9
+ function refuseLockedSession(json, options = {}) {
10
+ if (macosSessionLocked() !== true) return null;
11
+ const error = { code: "SESSION_LOCKED", message: SESSION_LOCKED_MESSAGE, userAction: SESSION_LOCKED_USER_ACTION };
12
+ if (optionFlag(options, "jsonStream")) emitRunStreamError(error);
13
+ emitProofFailure(json, { recoverable: false, error });
14
+ return EXIT.infra;
15
+ }
16
+ function emitRunStreamError(error) {
17
+ const line = JSON.stringify({ schemaVersion: 1, command: "run", event: "error", error, ts: (/* @__PURE__ */ new Date()).toISOString() });
18
+ Writable.prototype.write.call(process.stdout, `${line}
19
+ `);
20
+ }
21
+ export {
22
+ refuseLockedSession
23
+ };
@@ -1,23 +1,7 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
- import { recipeRuntimePath } from "@farmslot/recipe-cli/harness";
2
+ import { WEB_DAPP_CONSOLE_FILES, webDappAdapterId, webDappRuntimeFile } from "@farmslot/adapter-web/web-dapp";
4
3
  import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
5
4
  import { ensureConsoleCollector } from "../console-capture.js";
6
- import { webDappAdapterId } from "../../../adapters/web-dapp/lib/policy.mjs";
7
- const WEB_DAPP_CONSOLE_FILES = Object.freeze({
8
- app: "app-console.log",
9
- extension: "extension-console.log",
10
- collectorLog: "console-tail.log",
11
- pidFile: "console-tail.pid"
12
- });
13
- function webDappRuntimeFile(target, name) {
14
- let root = path.resolve(target);
15
- try {
16
- root = fs.realpathSync(root);
17
- } catch {
18
- }
19
- return recipeRuntimePath(root, webDappAdapterId(), name);
20
- }
21
5
  function webDappCaptureFiles(target) {
22
6
  return {
23
7
  extensionLog: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension),
@@ -54,10 +38,8 @@ async function verifyWebDappConsoleControl(capture) {
54
38
  return verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
55
39
  }
56
40
  export {
57
- WEB_DAPP_CONSOLE_FILES,
58
41
  ensureWebDappConsoleCapture,
59
42
  verifyWebDappConsoleControl,
60
43
  webDappCaptureFiles,
61
- webDappCdpPort,
62
- webDappRuntimeFile
44
+ webDappCdpPort
63
45
  };
@@ -1,44 +1,43 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
3
+ import { ownsProcess, webDappRuntimeFile } from "@farmslot/adapter-web/web-dapp";
2
4
  import { isRecord } from "@farmslot/recipe-cli/harness";
3
5
  import { verifyOwnedCollectorControl } from "../console-capture.js";
6
+ import { refuseLockedSession } from "../session-lock.js";
4
7
  import { devicePlatformFromEnv } from "../performance/device-platform.js";
5
8
  import {
6
9
  ensureWebDappConsoleCapture,
7
10
  webDappCaptureFiles,
8
11
  webDappCdpPort,
9
- webDappRuntimeFile,
10
12
  verifyWebDappConsoleControl
11
13
  } from "./console-capture.js";
14
+ async function webDappRunHeadless(target) {
15
+ const stateFile = webDappRuntimeFile(target, "browser.json");
16
+ try {
17
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
18
+ if (isRecord(state) && isRecord(state.browser) && typeof state.browser.headless === "boolean") {
19
+ if (ownsProcess("browser", state.browserPid, path.dirname(stateFile))) return state.browser.headless;
20
+ }
21
+ } catch {
22
+ }
23
+ try {
24
+ const { resolveHeadless } = await import("../../../library/actions/web-dapp/platform/adapter-web-lib.mjs");
25
+ return resolveHeadless();
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ const webDappRun = {
31
+ runtimeCheck: ({ target, options, json }) => async () => await webDappRunHeadless(target) === false ? refuseLockedSession(json, options) : null
32
+ };
12
33
  const webDappDiagnostics = {
13
34
  console: {
14
35
  start: (projectRoot) => ensureWebDappConsoleCapture(projectRoot),
15
36
  files: (projectRoot) => webDappCaptureFiles(projectRoot),
16
37
  cdpPort: () => webDappCdpPort(),
17
38
  verifyControl: (capture) => verifyOwnedCollectorControl(capture, () => verifyWebDappConsoleControl(capture))
18
- },
19
- requestLog: {
20
- path: (projectRoot) => webDappRuntimeFile(projectRoot, "wallet-requests.jsonl"),
21
- findings: (lines) => walletRequestFindings(lines)
22
39
  }
23
40
  };
24
- function walletRequestFindings(lines) {
25
- const findings = [];
26
- for (const line of lines) {
27
- let entry;
28
- try {
29
- entry = JSON.parse(line);
30
- } catch {
31
- continue;
32
- }
33
- if (!isRecord(entry)) continue;
34
- let text = null;
35
- if (entry.kind === "blocked-mainnet" && entry.probe !== true) text = `blocked-mainnet: ${String(entry.transport ?? "request")} to ${String(entry.url ?? "?")}`;
36
- else if (entry.kind === "refused-mainnet") text = `refused-mainnet: ${String(entry.method ?? "?")} (${String(entry.reason ?? "?")})`;
37
- else if (entry.kind === "unattributed") text = `unattributed wallet ${entry.binding === "request" ? "request" : "log entry"}${typeof entry.method === "string" ? ` (${entry.method})` : ""}`;
38
- if (text) findings.push({ level: "error", source: "wallet", text });
39
- }
40
- return findings;
41
- }
42
41
  const webDappObservation = {
43
42
  network: {
44
43
  backend: async (target, env, artifactsDir) => (await import("../extension/network-observer.js")).createLoadedExtensionNetworkBackend(
@@ -59,5 +58,7 @@ const webDappObservation = {
59
58
  };
60
59
  export {
61
60
  webDappDiagnostics,
62
- webDappObservation
61
+ webDappObservation,
62
+ webDappRun,
63
+ webDappRunHeadless
63
64
  };
@@ -1,82 +1,38 @@
1
- import { shellQuote } from "@farmslot/recipe-cli/harness";
2
1
  import path from "node:path";
3
2
  import { defineAdapter } from "@farmslot/adapter-sdk";
4
- import fs from "node:fs";
5
- import { POLICY_ENV, webDappAdapterId, webDappPolicy } from "../../../adapters/web-dapp/lib/policy.mjs";
6
- import { WEB_DAPP_CONSOLE_FILES, webDappRuntimeFile } from "./console-capture.js";
7
- import { stopWebDappBrowserSync } from "./stop.js";
3
+ import {
4
+ bindWebDappPolicy as bindPolicy,
5
+ createWebDappAdapter,
6
+ POLICY_ENV,
7
+ SIGNER_MODULE_ENV,
8
+ webDappAdapterId,
9
+ webDappPolicy
10
+ } from "@farmslot/adapter-web/web-dapp";
11
+ import { runnerDir } from "../../paths.js";
8
12
  import { adapterRegistry } from "../registry.js";
9
13
  import { webDappActions } from "./actions.js";
10
- import { webDappHarness, webDappRuntimeContext } from "./overlay.js";
11
- import { webDappReadiness } from "./readiness.js";
12
- import { webDappDiagnostics, webDappObservation } from "./run.js";
13
- function portFromEnv(...names) {
14
- for (const name of names) {
15
- const raw = process.env[name];
16
- if (raw && /^\d+$/u.test(raw)) return Number(raw);
17
- }
18
- return void 0;
19
- }
20
- function webDappAppPort() {
21
- return portFromEnv("TERMINAL_APP_PORT", "RECIPE_WATCHER_PORT", "WATCHER_PORT");
22
- }
23
- function webDappCdpPort() {
24
- return portFromEnv("RECIPE_CDP_PORT", "CDP_PORT");
25
- }
26
- function webDappSignerFor(target) {
27
- try {
28
- const state = JSON.parse(fs.readFileSync(webDappRuntimeFile(target, "browser.json"), "utf8"));
29
- if (typeof state.signer === "string") return state.signer;
30
- } catch {
31
- }
32
- return process.env.TERMINAL_SIGNER || "extension";
33
- }
14
+ import { webDappDiagnostics, webDappObservation, webDappRun } from "./run.js";
15
+ const METAMASK_SIGNER_MODULE = path.join(runnerDir, "adapters", "web-dapp", "metamask-signer.mjs");
34
16
  function bindWebDappPolicy(adapter) {
17
+ bindPolicy(adapter);
35
18
  if (adapter.extends === "web-dapp" && typeof adapter.policy?.module === "string") {
36
- process.env[POLICY_ENV] = adapter.policy.module;
19
+ process.env[SIGNER_MODULE_ENV] = METAMASK_SIGNER_MODULE;
37
20
  }
38
21
  return adapter;
39
22
  }
40
- const webDappSurface = defineAdapter({
23
+ const generic = createWebDappAdapter({
41
24
  id: "web-dapp",
42
- sdkVersion: 1,
43
- headless: false,
44
- resolveSlotPorts() {
45
- },
46
- async runtimeStatus(target) {
47
- if (!process.env[POLICY_ENV]) {
48
- return {
49
- decision: "blocked",
50
- reasonCode: "web-dapp-venue-policy",
51
- reasons: ["web-dapp needs a venue policy: select an adapter that extends it"],
52
- nextAction: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH",
53
- deps: "missing",
54
- devServer: { label: "next", status: "down" }
55
- };
56
- }
57
- const policy = webDappPolicy();
58
- const { webDappReadiness: webDappReadiness2 } = await import("../../../adapters/web-dapp/lib/readiness.mjs");
59
- const report = await webDappReadiness2({
60
- policy,
61
- target: path.resolve(target),
62
- appPort: webDappAppPort(),
63
- cdpPort: webDappCdpPort(),
64
- signer: process.env.TERMINAL_SIGNER || "extension"
65
- });
66
- const blocking = report.checks.filter((check) => check.required && check.status === "fail");
67
- const devServer = report.checks.find((check) => check.id === "app-dev-server");
68
- return {
69
- decision: blocking.length === 0 ? "ready" : "blocked",
70
- reasonCode: blocking[0]?.id,
71
- reasons: (blocking.length ? blocking : report.checks).map((check) => `${check.id}: ${check.detail}`),
72
- nextAction: blocking.length === 0 ? void 0 : blocking[0].id === "app-dev-server" || blocking[0].id === "testnet-forced" ? "start the slot dev server with testnet forced (Farmslot preflight), then retry" : `mm-harness doctor --adapter ${policy.adapterId} --target ${shellQuote(path.resolve(target))} --json`,
73
- deps: report.checks.find((check) => check.id === "dependencies")?.status === "pass" ? "current" : "missing",
74
- devServer: {
75
- label: "next",
76
- status: devServer?.status === "pass" ? "up" : "down"
77
- }
78
- };
79
- },
25
+ signerModule: METAMASK_SIGNER_MODULE,
26
+ cli: "mm-harness",
27
+ hooks: {
28
+ actions: webDappActions,
29
+ run: webDappRun,
30
+ diagnostics: webDappDiagnostics,
31
+ observation: webDappObservation
32
+ }
33
+ });
34
+ const webDappSurface = defineAdapter({
35
+ ...generic,
80
36
  runwayProvision: {
81
37
  async run(target, options) {
82
38
  return {
@@ -94,41 +50,12 @@ const webDappSurface = defineAdapter({
94
50
  };
95
51
  }
96
52
  },
97
- devServer: {
98
- label: "browser",
99
- describe: () => `${webDappAdapterId()} slot browser and wallet host (the Next.js dev server belongs to the slot)`,
100
- stop(target) {
101
- const result = stopWebDappBrowserSync(target);
102
- return { kind: "stopped", status: result.status, summary: result.summary, output: result.output };
103
- }
104
- },
105
- logSources(target) {
106
- return [
107
- { label: "app-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) },
108
- ...webDappSignerFor(target) === "extension" ? [{ label: "extension-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension) }] : [],
109
- { label: "wallet-requests", path: webDappRuntimeFile(target, "wallet-requests.jsonl") },
110
- { label: "wallet-host", path: webDappRuntimeFile(target, "wallet-host.log") },
111
- { label: "browser", path: webDappRuntimeFile(target, "browser.log") },
112
- { label: "next", path: path.join(target, "temp/farmslot/next-dev.log") }
113
- ];
114
- },
115
- // The app page's console and uncaught exceptions, as the Extension's
116
- // diagnostics read the extension console: errors make a run REVIEW.
117
- appLogSource(target) {
118
- return { label: "app-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) };
119
- },
120
- flags: { launch: ["freshProfile", "headless", "headful"] },
121
- hints: {
122
- // Bare web-dapp has no venue: every hint names the adapter that extends it.
123
- launch: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH",
124
- relaunch: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with --fresh-profile",
125
- runtimeProbeRecovery: () => "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH"
53
+ // adapter-web's slot readiness (with MetaMask's own checks) and capture providers.
54
+ readiness: {
55
+ liveChecks: generic.readiness.liveChecks,
56
+ captureProviders: generic.readiness.captureProviders,
57
+ compatibilityMode: (target) => process.env[POLICY_ENV] && webDappPolicy().checkout.matches(target) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge"
126
58
  },
127
- launch: async (context) => (await import("./launch.js")).launchWebDapp(context),
128
- actions: webDappActions,
129
- harness: webDappHarness,
130
- runtimeContext: webDappRuntimeContext,
131
- readiness: webDappReadiness,
132
59
  fixtures: { operations: ["init", "sync", "set"] },
133
60
  debug: {
134
61
  args: ({ devMenu }) => [
@@ -136,14 +63,11 @@ const webDappSurface = defineAdapter({
136
63
  "--action",
137
64
  devMenu ? "dev-menu" : "debug"
138
65
  ]
139
- },
140
- diagnostics: webDappDiagnostics,
141
- observation: webDappObservation
66
+ }
142
67
  });
143
68
  adapterRegistry.register(webDappSurface);
144
69
  export {
70
+ METAMASK_SIGNER_MODULE,
145
71
  bindWebDappPolicy,
146
- webDappAppPort,
147
- webDappCdpPort,
148
72
  webDappSurface
149
73
  };