@deeeed/metamask-harness 0.82.1 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +8 -1
- package/adapters/core/cleanup.sh +5 -29
- package/adapters/core/controller-import-probe.mjs +26 -0
- package/adapters/manifest.json +22 -102
- package/adapters/shared/session-lock.mjs +43 -0
- package/adapters/web-dapp/metamask-signer.mjs +212 -0
- package/dist/adapters/core/actions.js +0 -3
- package/dist/adapters/core/overlay.js +1 -5
- package/dist/adapters/core/readiness.js +133 -2
- package/dist/adapters/core/surface.js +53 -41
- package/dist/adapters/extension/extension-id.js +1 -0
- package/dist/adapters/extension/fixtures.js +60 -1
- package/dist/adapters/extension/network-observer.js +19 -206
- package/dist/adapters/extension/run.js +5 -3
- package/dist/adapters/extension/runtime.js +112 -20
- package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
- package/dist/adapters/mobile/devices.js +5 -1
- package/dist/adapters/mobile/performance-observer.js +9 -6
- package/dist/adapters/mobile/run.js +1 -1
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/performance/trace-kinds.js +36 -0
- package/dist/adapters/plugin-surface.js +34 -34
- package/dist/adapters/session-lock.js +23 -0
- package/dist/adapters/web-dapp/console-capture.js +2 -20
- package/dist/adapters/web-dapp/run.js +31 -25
- package/dist/adapters/web-dapp/surface.js +32 -108
- package/dist/command-contract.js +1363 -630
- package/dist/commands/core-readiness.js +1 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/launch/device.js +1 -1
- package/dist/doctor.js +5 -2
- package/dist/mm-harness-cli.js +228 -1210
- package/dist/performance-observation.js +28 -5
- package/docs/CONTRIBUTING.md +6 -4
- package/docs/cli-contract.md +1 -1
- package/docs/feature-guard.md +1 -1
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/extension/wallet/import.mjs +21 -4
- package/library/actions/extension/wallet/visible-session.mjs +32 -12
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/web-dapp/app/launch.mjs +1 -1
- package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
- package/library/actions/web-dapp/platform/hud.mjs +1 -1
- package/library/actions/web-dapp/platform/page.mjs +2 -3
- package/library/actions/web-dapp/platform/runtime.mjs +29 -178
- package/package.json +7 -6
- package/scripts/completions.sh +1 -1
- package/adapters/web-dapp/cleanup.mjs +0 -69
- package/adapters/web-dapp/inject.mjs +0 -75
- package/adapters/web-dapp/launch.mjs +0 -631
- package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
- package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
- package/adapters/web-dapp/lib/display.mjs +0 -115
- package/adapters/web-dapp/lib/hud.mjs +0 -148
- package/adapters/web-dapp/lib/policy.mjs +0 -79
- package/adapters/web-dapp/lib/processes.mjs +0 -187
- package/adapters/web-dapp/lib/readiness.mjs +0 -272
- package/adapters/web-dapp/stop.mjs +0 -38
- package/adapters/web-dapp/verify.mjs +0 -51
- package/adapters/web-dapp/wallet-host.mjs +0 -520
- package/dist/adapters/core/live-script-env.js +0 -36
- package/dist/adapters/extension/browser-cdp.js +0 -174
- package/dist/adapters/extension/performance-observer.js +0 -75
- package/dist/adapters/performance/cdp-trace.js +0 -406
- package/dist/adapters/performance/js-task-metrics.js +0 -35
- package/dist/adapters/web-dapp/doctor-checks.js +0 -33
- package/dist/adapters/web-dapp/launch.js +0 -87
- package/dist/adapters/web-dapp/overlay.js +0 -21
- package/dist/adapters/web-dapp/readiness.js +0 -10
- package/dist/adapters/web-dapp/stop.js +0 -20
- package/dist/async.js +0 -19
- package/dist/cli.js +0 -202
|
@@ -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
|
|
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
|
-
|
|
904
|
-
|
|
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(
|
|
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(
|
|
916
|
-
if (listenerPids.length > 0 || processExists(
|
|
969
|
+
} while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < termDeadline);
|
|
970
|
+
if (listenerPids.length > 0 || processExists(pid)) {
|
|
917
971
|
try {
|
|
918
|
-
process.kill(
|
|
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(
|
|
980
|
+
} while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < killDeadline);
|
|
927
981
|
}
|
|
928
|
-
if (listenerPids.length > 0 || processExists(
|
|
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 {
|
|
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
|
|
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 "
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
};
|