@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +8 -1
  3. package/adapters/core/cleanup.sh +5 -29
  4. package/adapters/core/controller-import-probe.mjs +26 -0
  5. package/adapters/manifest.json +22 -102
  6. package/adapters/shared/session-lock.mjs +43 -0
  7. package/adapters/web-dapp/metamask-signer.mjs +212 -0
  8. package/dist/adapters/core/actions.js +0 -3
  9. package/dist/adapters/core/overlay.js +1 -5
  10. package/dist/adapters/core/readiness.js +133 -2
  11. package/dist/adapters/core/surface.js +53 -41
  12. package/dist/adapters/extension/extension-id.js +1 -0
  13. package/dist/adapters/extension/fixtures.js +60 -1
  14. package/dist/adapters/extension/network-observer.js +19 -206
  15. package/dist/adapters/extension/run.js +5 -3
  16. package/dist/adapters/extension/runtime.js +112 -20
  17. package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
  18. package/dist/adapters/mobile/devices.js +5 -1
  19. package/dist/adapters/mobile/performance-observer.js +9 -6
  20. package/dist/adapters/mobile/run.js +1 -1
  21. package/dist/adapters/mobile/surface.js +1 -2
  22. package/dist/adapters/performance/trace-kinds.js +36 -0
  23. package/dist/adapters/plugin-surface.js +34 -34
  24. package/dist/adapters/session-lock.js +23 -0
  25. package/dist/adapters/web-dapp/console-capture.js +2 -20
  26. package/dist/adapters/web-dapp/run.js +31 -25
  27. package/dist/adapters/web-dapp/surface.js +32 -108
  28. package/dist/command-contract.js +1363 -630
  29. package/dist/commands/core-readiness.js +1 -9
  30. package/dist/commands/doctor.js +1 -1
  31. package/dist/commands/launch/device.js +1 -1
  32. package/dist/doctor.js +5 -2
  33. package/dist/mm-harness-cli.js +228 -1210
  34. package/dist/performance-observation.js +28 -5
  35. package/docs/CONTRIBUTING.md +6 -4
  36. package/docs/cli-contract.md +1 -1
  37. package/docs/feature-guard.md +1 -1
  38. package/library/actions/core/perps/_controller.mjs +10 -0
  39. package/library/actions/extension/wallet/import.mjs +21 -4
  40. package/library/actions/extension/wallet/visible-session.mjs +32 -12
  41. package/library/actions/shared/console-collector.mjs +1 -1
  42. package/library/actions/web-dapp/app/launch.mjs +1 -1
  43. package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
  44. package/library/actions/web-dapp/platform/hud.mjs +1 -1
  45. package/library/actions/web-dapp/platform/page.mjs +2 -3
  46. package/library/actions/web-dapp/platform/runtime.mjs +29 -178
  47. package/package.json +7 -6
  48. package/scripts/completions.sh +1 -1
  49. package/adapters/web-dapp/cleanup.mjs +0 -69
  50. package/adapters/web-dapp/inject.mjs +0 -75
  51. package/adapters/web-dapp/launch.mjs +0 -631
  52. package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
  53. package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
  54. package/adapters/web-dapp/lib/display.mjs +0 -115
  55. package/adapters/web-dapp/lib/hud.mjs +0 -148
  56. package/adapters/web-dapp/lib/policy.mjs +0 -79
  57. package/adapters/web-dapp/lib/processes.mjs +0 -187
  58. package/adapters/web-dapp/lib/readiness.mjs +0 -272
  59. package/adapters/web-dapp/stop.mjs +0 -38
  60. package/adapters/web-dapp/verify.mjs +0 -51
  61. package/adapters/web-dapp/wallet-host.mjs +0 -520
  62. package/dist/adapters/core/live-script-env.js +0 -36
  63. package/dist/adapters/extension/browser-cdp.js +0 -174
  64. package/dist/adapters/extension/performance-observer.js +0 -75
  65. package/dist/adapters/performance/cdp-trace.js +0 -406
  66. package/dist/adapters/performance/js-task-metrics.js +0 -35
  67. package/dist/adapters/web-dapp/doctor-checks.js +0 -33
  68. package/dist/adapters/web-dapp/launch.js +0 -87
  69. package/dist/adapters/web-dapp/overlay.js +0 -21
  70. package/dist/adapters/web-dapp/readiness.js +0 -10
  71. package/dist/adapters/web-dapp/stop.js +0 -20
  72. package/dist/async.js +0 -19
  73. 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(
@@ -40,22 +41,39 @@ async function prepareExtensionRuntime(options) {
40
41
  projectRoot,
41
42
  cdpPort,
42
43
  timeoutMs: options.healthTimeoutMs,
43
- pageMode: options.pageMode
44
+ pageMode: options.pageMode,
45
+ // The validation launch seeds the copied wallet fixture into a fresh
46
+ // profile; attaching to an existing runtime may legitimately be onboarding.
47
+ seededLaunchDir: seededLaunchDir(projectRoot, launch)
44
48
  });
45
49
  return { launch, health };
46
50
  }
51
+ function seededLaunchDir(projectRoot, launch) {
52
+ return launch?.runtimeDir ? path.join(projectRoot, launch.runtimeDir, "live") : void 0;
53
+ }
54
+ const FIXTURE_SEED_RECHECK_MS = 5e3;
47
55
  async function assertHealthyExtensionRuntime(options) {
48
56
  const timeoutMs = options.timeoutMs ?? 3e4;
49
57
  const deadline = Date.now() + timeoutMs;
50
58
  let lastReport = null;
59
+ let onboardingSince;
51
60
  while (Date.now() <= deadline) {
52
61
  lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort, {
53
- pageMode: options.pageMode
62
+ pageMode: options.pageMode,
63
+ seededLaunchDir: options.seededLaunchDir
54
64
  });
55
65
  if (lastReport.status === "PASS") return lastReport;
56
- 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") {
57
67
  throw new Error(formatHealthFailure(lastReport, options.projectRoot));
58
68
  }
69
+ if (lastReport.errorCode === "FIXTURE_SEED_NOT_APPLIED") {
70
+ onboardingSince ??= Date.now();
71
+ if (Date.now() - onboardingSince >= FIXTURE_SEED_RECHECK_MS) {
72
+ throw new Error(formatHealthFailure(lastReport, options.projectRoot));
73
+ }
74
+ } else {
75
+ onboardingSince = void 0;
76
+ }
59
77
  await sleep(500);
60
78
  }
61
79
  const report = lastReport ?? failReport(options.cdpPort, ["CDP runtime was not probed."], {});
@@ -102,6 +120,24 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
102
120
  if (String(target.url ?? "").startsWith("chrome-error://")) {
103
121
  findings.push(`Extension target resolved to chrome-error page: ${target.url}`);
104
122
  }
123
+ if (options.seededLaunchDir && String(target.url ?? "").includes("/onboarding/")) {
124
+ return {
125
+ status: "FAIL",
126
+ errorCode: "FIXTURE_SEED_NOT_APPLIED",
127
+ 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.`,
128
+ cdpPort,
129
+ targetUrl: target.url,
130
+ extensionId: safeExtensionId(target),
131
+ extensionPageTargets: extensionTargets.length,
132
+ findings: [
133
+ "FIXTURE_SEED_NOT_APPLIED: MetaMask is still on onboarding after launch: the wallet fixture was not applied."
134
+ ],
135
+ details: {
136
+ projectRoot,
137
+ targetUrls: targets.map((entry) => entry.url).filter(Boolean)
138
+ }
139
+ };
140
+ }
105
141
  let session;
106
142
  try {
107
143
  session = await CdpSession.connect(target.webSocketDebuggerUrl, {
@@ -114,18 +150,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
114
150
  Math.min(1e3, cdpCallTimeoutMs)
115
151
  );
116
152
  if (compositor.status === "suspended") {
117
- 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;
118
155
  return {
119
156
  status: "FAIL",
120
- errorCode: "UI_COMPOSITOR_SUSPENDED",
121
- userAction,
157
+ errorCode: locked ? "SESSION_LOCKED" : "UI_COMPOSITOR_SUSPENDED",
158
+ userAction: locked ? SESSION_LOCKED_USER_ACTION : compositorRecoveryAction(projectRoot),
122
159
  cdpPort,
123
160
  targetUrl: target.url,
124
161
  extensionId: safeExtensionId(target),
125
162
  extensionPageTargets: extensionTargets.length,
126
- findings: [
127
- `UI_COMPOSITOR_SUSPENDED: the Extension page is reachable over CDP but its render compositor is not advancing (${compositor.reason ?? "unknown reason"}).`
128
- ],
163
+ findings: locked ? [`SESSION_LOCKED: ${SESSION_LOCKED_MESSAGE}`, suspended] : [suspended],
129
164
  details: {
130
165
  projectRoot,
131
166
  targetUrls: targets.map((entry) => entry.url).filter(Boolean),
@@ -634,7 +669,7 @@ async function launchLockedValidationBrowser(options) {
634
669
  await stopOwnedCdpListener(options.cdpPort, [
635
670
  validationRuntimePath,
636
671
  path.join(options.projectRoot, recipeRuntimeDir())
637
- ]);
672
+ ], path.join(options.projectRoot, recipeRuntimeDir()));
638
673
  assertNoLiveValidationProfileOwners(validationProfileRoot);
639
674
  pruneValidationRuns(validationProfileRoot, 0);
640
675
  pruneValidationRuns(validationArtifactsRoot, 4);
@@ -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",
@@ -895,16 +933,32 @@ function runProcess(command, args, options) {
895
933
  });
896
934
  });
897
935
  }
898
- async function stopOwnedCdpListener(cdpPort, runtimePaths) {
936
+ async function stopOwnedCdpListener(cdpPort, runtimePaths, checkoutRuntimeRoot) {
899
937
  if (process.platform !== "darwin" && process.platform !== "linux") return;
900
938
  let listenerPids = await inspectCdpListenerPids(cdpPort);
901
939
  if (listenerPids.length === 0) return;
902
940
  const identity = runtimePaths.map((runtimePath) => readValidationRuntimeIdentity(runtimePath, cdpPort)).find((candidate) => candidate?.pid === listenerPids[0]);
903
- if (!identity || listenerPids.length !== 1 || listenerPids[0] !== identity.pid || !await browserEchoesRuntimeNonce(cdpPort, identity.nonce)) {
904
- 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
+ }
905
959
  }
906
960
  try {
907
- process.kill(identity.pid, "SIGTERM");
961
+ process.kill(pid, "SIGTERM");
908
962
  } catch (error) {
909
963
  if (error.code !== "ESRCH") throw error;
910
964
  }
@@ -912,10 +966,10 @@ async function stopOwnedCdpListener(cdpPort, runtimePaths) {
912
966
  do {
913
967
  await sleep(50);
914
968
  listenerPids = await inspectCdpListenerPids(cdpPort);
915
- } while ((listenerPids.length > 0 || processExists(identity.pid)) && Date.now() < termDeadline);
916
- if (listenerPids.length > 0 || processExists(identity.pid)) {
969
+ } while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < termDeadline);
970
+ if (listenerPids.length > 0 || processExists(pid)) {
917
971
  try {
918
- process.kill(identity.pid, "SIGKILL");
972
+ process.kill(pid, "SIGKILL");
919
973
  } catch (error) {
920
974
  if (error.code !== "ESRCH") throw error;
921
975
  }
@@ -923,12 +977,48 @@ async function stopOwnedCdpListener(cdpPort, runtimePaths) {
923
977
  do {
924
978
  await sleep(50);
925
979
  listenerPids = await inspectCdpListenerPids(cdpPort);
926
- } while ((listenerPids.length > 0 || processExists(identity.pid)) && Date.now() < killDeadline);
980
+ } while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < killDeadline);
927
981
  }
928
- if (listenerPids.length > 0 || processExists(identity.pid)) {
982
+ if (listenerPids.length > 0 || processExists(pid)) {
929
983
  throw new Error(`Extension validation could not stop its CDP listener on port ${cdpPort}.`);
930
984
  }
931
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
+ }
932
1022
  async function inspectCdpListenerPids(cdpPort) {
933
1023
  const result = await runProcess("lsof", ["-ti", `tcp:${cdpPort}`, "-sTCP:LISTEN"], {
934
1024
  cwd: runnerDir,
@@ -1001,5 +1091,7 @@ export {
1001
1091
  evmRpcRecoveryAction,
1002
1092
  extensionBackgroundProbeTimeoutMs,
1003
1093
  formatHealthFailure,
1004
- prepareExtensionRuntime
1094
+ prepareExtensionRuntime,
1095
+ seededLaunchDir,
1096
+ stopOwnedCdpListener
1005
1097
  };
@@ -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;
@@ -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,
@@ -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(
@@ -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];
@@ -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,12 +1,9 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
- import {
3
- adapterPlugin,
4
- adapterSelectionFailureOut,
5
- ensureAdapterLoaded,
6
- selectedAdapterId
7
- } 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";
5
+ import { libraryAdapterFiles } from "@farmslot/recipe-runner";
8
6
  import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
9
- import { adapterRegistry } from "./registry.js";
10
7
  import { bindWebDappPolicy } from "./web-dapp/surface.js";
11
8
  function configuredLibrarySources() {
12
9
  let libraries;
@@ -25,39 +22,41 @@ function operatorLibraryEnv() {
25
22
  else env.RECIPE_LIBRARY_PATH = declared.value;
26
23
  return env;
27
24
  }
28
- async function loadSelectedAdapter(command, args) {
29
- const passthrough = args.indexOf("--");
30
- const own = passthrough < 0 ? args : args.slice(0, passthrough);
31
- const libraries = [];
32
- own.forEach((arg, index) => {
33
- if (arg === "--library" && own[index + 1] !== void 0) libraries.push(own[index + 1]);
34
- else if (arg.startsWith("--library=")) libraries.push(arg.slice("--library=".length));
35
- });
36
- const selected = selectedAdapterId(args);
25
+ const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
26
+ let boundPolicy;
27
+ async function fenceBoundPolicy() {
28
+ const bound = boundPolicy;
29
+ boundPolicy = void 0;
30
+ if (!bound || process.env[POLICY_ENV] !== bound.file) return;
31
+ const plugin = adapterPlugin(bound.adapterId);
37
32
  try {
38
- await ensureAdapterLoaded(selected, {
39
- libraries,
40
- configured: configuredLibrarySources(),
41
- env: operatorLibraryEnv(),
42
- adopt: adoptPluginAdapter
43
- });
44
- return void 0;
33
+ if (!plugin) throw new PolicyFenceError(`no plugin record for adapter '${bound.adapterId}'`);
34
+ const files = await libraryAdapterFiles(plugin.root, { module: plugin.module });
35
+ const covered = new Set(files.map((file) => fs.realpathSync(path.join(plugin.root, file))));
36
+ const scope = `${path.posix.dirname(plugin.module.replace(/^\.\//u, ""))}/ and actions/ in ${plugin.root}`;
37
+ process.env[POLICY_DIGEST_ENV] = fencePolicy({ file: bound.file, covered, scope });
45
38
  } catch (error) {
46
- const refusal = error;
47
- if (typeof refusal.code !== "string" || typeof refusal.userAction !== "string") throw error;
48
- if (selected !== void 0 && adapterRegistry.has(selected) && refusal.code !== "ADAPTER_ID_CONFLICT") {
49
- return void 0;
39
+ delete process.env[POLICY_ENV];
40
+ delete process.env[POLICY_DIGEST_ENV];
41
+ throw error;
42
+ }
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
50
  }
51
- return adapterSelectionFailureOut(own.includes("--json"), command, {
52
- code: refusal.code,
53
- message: String(refusal.message),
54
- userAction: refusal.userAction
55
- });
51
+ throw error;
56
52
  }
57
53
  }
58
- const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
59
54
  function adoptPluginAdapter(adapter) {
60
55
  const surface = bindWebDappPolicy(adapter);
56
+ const policy = adapter;
57
+ if (policy.extends === "web-dapp" && typeof policy.policy?.module === "string") {
58
+ boundPolicy = { adapterId: adapter.id, file: policy.policy.module };
59
+ }
61
60
  return {
62
61
  ...surface,
63
62
  run: {
@@ -92,7 +91,8 @@ function adoptPluginAdapter(adapter) {
92
91
  export {
93
92
  ADAPTER_PLUGIN_ENV,
94
93
  adoptPluginAdapter,
94
+ afterAdapterLoad,
95
95
  configuredLibrarySources,
96
- loadSelectedAdapter,
96
+ fenceBoundPolicy,
97
97
  operatorLibraryEnv
98
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,46 +1,50 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ownsProcess, webDappRuntimeFile } from "@farmslot/adapter-web/web-dapp";
1
4
  import { isRecord } from "@farmslot/recipe-cli/harness";
2
5
  import { verifyOwnedCollectorControl } from "../console-capture.js";
6
+ import { refuseLockedSession } from "../session-lock.js";
3
7
  import { devicePlatformFromEnv } from "../performance/device-platform.js";
4
8
  import {
5
9
  ensureWebDappConsoleCapture,
6
10
  webDappCaptureFiles,
7
11
  webDappCdpPort,
8
- webDappRuntimeFile,
9
12
  verifyWebDappConsoleControl
10
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
+ };
11
33
  const webDappDiagnostics = {
12
34
  console: {
13
35
  start: (projectRoot) => ensureWebDappConsoleCapture(projectRoot),
14
36
  files: (projectRoot) => webDappCaptureFiles(projectRoot),
15
37
  cdpPort: () => webDappCdpPort(),
16
38
  verifyControl: (capture) => verifyOwnedCollectorControl(capture, () => verifyWebDappConsoleControl(capture))
17
- },
18
- requestLog: {
19
- path: (projectRoot) => webDappRuntimeFile(projectRoot, "wallet-requests.jsonl"),
20
- findings: (lines) => walletRequestFindings(lines)
21
39
  }
22
40
  };
23
- function walletRequestFindings(lines) {
24
- const findings = [];
25
- for (const line of lines) {
26
- let entry;
27
- try {
28
- entry = JSON.parse(line);
29
- } catch {
30
- continue;
31
- }
32
- if (!isRecord(entry)) continue;
33
- let text = null;
34
- if (entry.kind === "blocked-mainnet" && entry.probe !== true) text = `blocked-mainnet: ${String(entry.transport ?? "request")} to ${String(entry.url ?? "?")}`;
35
- else if (entry.kind === "refused-mainnet") text = `refused-mainnet: ${String(entry.method ?? "?")} (${String(entry.reason ?? "?")})`;
36
- else if (entry.kind === "unattributed") text = `unattributed wallet ${entry.binding === "request" ? "request" : "log entry"}${typeof entry.method === "string" ? ` (${entry.method})` : ""}`;
37
- if (text) findings.push({ level: "error", source: "wallet", text });
38
- }
39
- return findings;
40
- }
41
41
  const webDappObservation = {
42
42
  network: {
43
- backend: async (_target, env, artifactsDir) => (await import("../extension/network-observer.js")).createExtensionNetworkBackend(env, artifactsDir)
43
+ backend: async (target, env, artifactsDir) => (await import("../extension/network-observer.js")).createLoadedExtensionNetworkBackend(
44
+ path.resolve(env.TERMINAL_EXTENSION_DIR || path.join(target, "temp/metamask-extension/chrome")),
45
+ env,
46
+ artifactsDir
47
+ )
44
48
  },
45
49
  performance: {
46
50
  start: async (context) => {
@@ -54,5 +58,7 @@ const webDappObservation = {
54
58
  };
55
59
  export {
56
60
  webDappDiagnostics,
57
- webDappObservation
61
+ webDappObservation,
62
+ webDappRun,
63
+ webDappRunHeadless
58
64
  };