@deeeed/metamask-harness 0.82.1 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +8 -1
- package/adapters/core/cleanup.sh +5 -29
- package/adapters/core/controller-import-probe.mjs +26 -0
- package/adapters/manifest.json +22 -102
- package/adapters/shared/session-lock.mjs +43 -0
- package/adapters/web-dapp/metamask-signer.mjs +212 -0
- package/dist/adapters/core/actions.js +0 -3
- package/dist/adapters/core/overlay.js +1 -5
- package/dist/adapters/core/readiness.js +133 -2
- package/dist/adapters/core/surface.js +53 -41
- package/dist/adapters/extension/extension-id.js +1 -0
- package/dist/adapters/extension/fixtures.js +60 -1
- package/dist/adapters/extension/network-observer.js +19 -206
- package/dist/adapters/extension/run.js +5 -3
- package/dist/adapters/extension/runtime.js +112 -20
- package/dist/adapters/mobile/android-gfxinfo-performance.js +3 -1
- package/dist/adapters/mobile/devices.js +5 -1
- package/dist/adapters/mobile/performance-observer.js +9 -6
- package/dist/adapters/mobile/run.js +1 -1
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/performance/trace-kinds.js +36 -0
- package/dist/adapters/plugin-surface.js +34 -34
- package/dist/adapters/session-lock.js +23 -0
- package/dist/adapters/web-dapp/console-capture.js +2 -20
- package/dist/adapters/web-dapp/run.js +31 -25
- package/dist/adapters/web-dapp/surface.js +32 -108
- package/dist/command-contract.js +1363 -630
- package/dist/commands/core-readiness.js +1 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/launch/device.js +1 -1
- package/dist/doctor.js +5 -2
- package/dist/mm-harness-cli.js +228 -1210
- package/dist/performance-observation.js +28 -5
- package/docs/CONTRIBUTING.md +6 -4
- package/docs/cli-contract.md +1 -1
- package/docs/feature-guard.md +1 -1
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/extension/wallet/import.mjs +21 -4
- package/library/actions/extension/wallet/visible-session.mjs +32 -12
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/web-dapp/app/launch.mjs +1 -1
- package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
- package/library/actions/web-dapp/platform/hud.mjs +1 -1
- package/library/actions/web-dapp/platform/page.mjs +2 -3
- package/library/actions/web-dapp/platform/runtime.mjs +29 -178
- package/package.json +7 -6
- package/scripts/completions.sh +1 -1
- package/adapters/web-dapp/cleanup.mjs +0 -69
- package/adapters/web-dapp/inject.mjs +0 -75
- package/adapters/web-dapp/launch.mjs +0 -631
- package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
- package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
- package/adapters/web-dapp/lib/display.mjs +0 -115
- package/adapters/web-dapp/lib/hud.mjs +0 -148
- package/adapters/web-dapp/lib/policy.mjs +0 -79
- package/adapters/web-dapp/lib/processes.mjs +0 -187
- package/adapters/web-dapp/lib/readiness.mjs +0 -272
- package/adapters/web-dapp/stop.mjs +0 -38
- package/adapters/web-dapp/verify.mjs +0 -51
- package/adapters/web-dapp/wallet-host.mjs +0 -520
- package/dist/adapters/core/live-script-env.js +0 -36
- package/dist/adapters/extension/browser-cdp.js +0 -174
- package/dist/adapters/extension/performance-observer.js +0 -75
- package/dist/adapters/performance/cdp-trace.js +0 -406
- package/dist/adapters/performance/js-task-metrics.js +0 -35
- package/dist/adapters/web-dapp/doctor-checks.js +0 -33
- package/dist/adapters/web-dapp/launch.js +0 -87
- package/dist/adapters/web-dapp/overlay.js +0 -21
- package/dist/adapters/web-dapp/readiness.js +0 -10
- package/dist/adapters/web-dapp/stop.js +0 -20
- package/dist/async.js +0 -19
- package/dist/cli.js +0 -202
|
@@ -1,11 +1,140 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
6
|
+
import { shellQuote } from "@farmslot/recipe-cli/harness";
|
|
4
7
|
import { resolveLiveTsx } from "../../live-tsx.js";
|
|
8
|
+
import { runnerDir } from "../../paths.js";
|
|
5
9
|
const coreReadiness = {
|
|
6
10
|
compatibilityMode: (target) => fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge",
|
|
7
|
-
checks: (target) => [coreControllerActionsCheck(target)]
|
|
11
|
+
checks: (target) => [coreControllerActionsCheck(target)],
|
|
12
|
+
// Installed dependencies and a tsx are not enough: a partial workspace build
|
|
13
|
+
// can still leave the controller unimportable. Import it the way the actions
|
|
14
|
+
// do, once the cheaper gate passes. runtimeStatus asks the same question, so
|
|
15
|
+
// --print-ready, --expect-live and verify agree.
|
|
16
|
+
liveChecks: async (target) => coreDependencyBlock(target) ? [] : [coreControllerImportCheck(await coreControllerImport(target))],
|
|
17
|
+
// doctor --fix cannot build the workspace: an unimportable controller stays failed.
|
|
18
|
+
runtimeBlock: (_target, runtime) => runtime?.reasonCode === "CORE_CONTROLLER_IMPORT_FAILED" ? { id: "core-controller-import", userAction: runtime.nextAction } : void 0
|
|
8
19
|
};
|
|
20
|
+
const CONTROLLER_IMPORT_TIMEOUT_MS = 6e4;
|
|
21
|
+
const PARENT_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
22
|
+
function isProbeReport(value) {
|
|
23
|
+
if (typeof value !== "object" || value === null) return false;
|
|
24
|
+
const report = value;
|
|
25
|
+
const optionalString = (field) => field === void 0 || field === null || typeof field === "string";
|
|
26
|
+
return typeof report.ok === "boolean" && optionalString(report.code) && optionalString(report.url) && (report.message === void 0 || typeof report.message === "string");
|
|
27
|
+
}
|
|
28
|
+
function coreRebuildAction(target) {
|
|
29
|
+
return `cd ${shellQuote(path.resolve(target))} && yarn build`;
|
|
30
|
+
}
|
|
31
|
+
const controllerImports = /* @__PURE__ */ new Map();
|
|
32
|
+
function coreControllerImport(target) {
|
|
33
|
+
const key = path.resolve(target);
|
|
34
|
+
let result = controllerImports.get(key);
|
|
35
|
+
if (!result) {
|
|
36
|
+
result = probeControllerImport(key);
|
|
37
|
+
controllerImports.set(key, result);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
async function probeControllerImport(target) {
|
|
42
|
+
const runner = resolveLiveTsx(target);
|
|
43
|
+
if (!runner) return { ok: false, reason: "no tsx runtime found" };
|
|
44
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mm-harness-core-probe-"));
|
|
45
|
+
let cancelledBy;
|
|
46
|
+
try {
|
|
47
|
+
const { coreSurface } = await import("./surface.js");
|
|
48
|
+
const env = { ...process.env, ...await coreSurface.actions.tsxLiveScripts?.env(target, tempDir) };
|
|
49
|
+
const probe = path.join(runnerDir, "adapters/core/controller-import-probe.mjs");
|
|
50
|
+
const run = await runBounded(runner.bin, [probe, target], target, env);
|
|
51
|
+
cancelledBy = run.cancelledBy;
|
|
52
|
+
if (run.spawnError) {
|
|
53
|
+
return { ok: false, reason: `tsx (${runner.source}) ${runner.bin} could not start: ${run.spawnError.code ?? run.spawnError.message}`, code: run.spawnError.code };
|
|
54
|
+
}
|
|
55
|
+
if (run.timedOut) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: `importing the Perps controller took over ${CONTROLLER_IMPORT_TIMEOUT_MS / 1e3}s`,
|
|
59
|
+
userAction: coreRebuildAction(target)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const line = run.stdout.trim().split("\n").at(-1) ?? "";
|
|
63
|
+
let parsed;
|
|
64
|
+
if (line.startsWith("{")) {
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(line);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const report = isProbeReport(parsed) ? parsed : void 0;
|
|
72
|
+
if (!report || run.exitCode !== 0) {
|
|
73
|
+
return { ok: false, reason: `tsx (${runner.source}) ${runner.bin} did not run the controller import probe (exit ${run.exitCode ?? "signal"})` };
|
|
74
|
+
}
|
|
75
|
+
if (report.ok) return { ok: true };
|
|
76
|
+
const missingModule = report.code === "ERR_MODULE_NOT_FOUND" || report.code === "MODULE_NOT_FOUND";
|
|
77
|
+
const detail = report.url ?? report.message?.split("\n")[0];
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: missingModule ? `a module is missing (${detail})` : String(detail),
|
|
81
|
+
code: report.code ?? void 0,
|
|
82
|
+
userAction: coreRebuildAction(target)
|
|
83
|
+
};
|
|
84
|
+
} finally {
|
|
85
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
86
|
+
if (cancelledBy) process.kill(process.pid, cancelledBy);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function coreControllerImportCheck(result) {
|
|
90
|
+
const id = "core-controller-import";
|
|
91
|
+
if (result.ok) {
|
|
92
|
+
return { id, status: "pass", required: true, message: "the Perps controller imports under the action runtime." };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
status: "fail",
|
|
97
|
+
required: true,
|
|
98
|
+
message: `metamask.perps.* actions cannot import the Perps controller: ${result.reason}.`,
|
|
99
|
+
...result.code ? { detail: result.code } : {},
|
|
100
|
+
...result.userAction ? { userAction: result.userAction } : {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function runBounded(bin, args, cwd, env) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const child = spawn(bin, args, { cwd, env, stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
106
|
+
let stdout = "";
|
|
107
|
+
let timedOut = false;
|
|
108
|
+
let cancelledBy;
|
|
109
|
+
const killGroup = () => {
|
|
110
|
+
if (child.pid === void 0) return;
|
|
111
|
+
try {
|
|
112
|
+
process.kill(-child.pid, "SIGKILL");
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error.code !== "ESRCH") throw error;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const onSignal = (signal) => {
|
|
118
|
+
cancelledBy ??= signal;
|
|
119
|
+
killGroup();
|
|
120
|
+
};
|
|
121
|
+
for (const signal of PARENT_SIGNALS) process.on(signal, onSignal);
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
timedOut = true;
|
|
124
|
+
killGroup();
|
|
125
|
+
}, CONTROLLER_IMPORT_TIMEOUT_MS);
|
|
126
|
+
const settle = (run) => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
for (const signal of PARENT_SIGNALS) process.off(signal, onSignal);
|
|
129
|
+
resolve({ ...run, ...cancelledBy ? { cancelledBy } : {} });
|
|
130
|
+
};
|
|
131
|
+
child.stdout.on("data", (chunk) => {
|
|
132
|
+
stdout += chunk.toString();
|
|
133
|
+
});
|
|
134
|
+
child.on("error", (error) => settle({ stdout, exitCode: null, timedOut: false, spawnError: error }));
|
|
135
|
+
child.on("close", (code) => settle({ stdout, exitCode: code, timedOut }));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
9
138
|
function coreControllerActionsCheck(target) {
|
|
10
139
|
const block = coreDependencyBlock(target);
|
|
11
140
|
if (block) {
|
|
@@ -31,5 +160,7 @@ function coreControllerActionsCheck(target) {
|
|
|
31
160
|
};
|
|
32
161
|
}
|
|
33
162
|
export {
|
|
34
|
-
|
|
163
|
+
coreControllerImport,
|
|
164
|
+
coreReadiness,
|
|
165
|
+
coreRebuildAction
|
|
35
166
|
};
|
|
@@ -1,20 +1,50 @@
|
|
|
1
|
-
import { shellQuote } from "@farmslot/recipe-cli/harness";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import {
|
|
2
|
+
import { checkoutWorkspacePackages, createNodeAdapter } from "@farmslot/adapter-node";
|
|
4
3
|
import { defineAdapter } from "@farmslot/adapter-sdk";
|
|
4
|
+
import { shellQuote } from "@farmslot/recipe-cli/harness";
|
|
5
|
+
import { depsCheck } from "@farmslot/recipe-runner/runtime/deps-readiness";
|
|
6
|
+
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
7
|
+
import { checkoutHas, checkoutHasDir } from "../checkout-files.js";
|
|
5
8
|
import { adapterRegistry } from "../registry.js";
|
|
6
9
|
import { coreActions } from "./actions.js";
|
|
7
|
-
import { coreHarness, corePrepare
|
|
8
|
-
import { coreReadiness } from "./readiness.js";
|
|
10
|
+
import { coreHarness, corePrepare } from "./overlay.js";
|
|
11
|
+
import { coreControllerImport, coreReadiness, coreRebuildAction } from "./readiness.js";
|
|
9
12
|
import { coreRun } from "./run.js";
|
|
10
|
-
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
11
|
-
import { checkoutHas, checkoutHasDir } from "../checkout-files.js";
|
|
12
13
|
const coreSurface = defineAdapter({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
...createNodeAdapter({
|
|
15
|
+
id: "core",
|
|
16
|
+
detect: {
|
|
17
|
+
files: (target) => checkoutHasDir(target, "packages/perps-controller") && checkoutHas(target, "yarn.lock")
|
|
18
|
+
},
|
|
19
|
+
hints: {
|
|
20
|
+
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
21
|
+
relaunch: "mm-harness verify",
|
|
22
|
+
runtimeProbeRecovery: (target) => `mm-harness verify --adapter core --target ${shellQuote(path.resolve(target))}`
|
|
23
|
+
},
|
|
24
|
+
wording: {
|
|
25
|
+
devServerStop: {
|
|
26
|
+
message: "core is headless \u2014 no dev server runs for a core checkout",
|
|
27
|
+
userAction: "use mm-harness verify (readiness) or mm-harness run (execute a recipe)"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
// Live scripts import the checkout's workspace packages from source: an
|
|
31
|
+
// unbuilt or stale checkout has no usable dist/ for them.
|
|
32
|
+
workspacePackages: checkoutWorkspacePackages,
|
|
33
|
+
actions: coreActions,
|
|
34
|
+
harness: coreHarness,
|
|
35
|
+
// coreRun's dependencyBlock (the Core gate) replaces adapter-node's check.
|
|
36
|
+
run: coreRun
|
|
37
|
+
}),
|
|
38
|
+
// Core's live adapter marks a module the checkout cannot resolve (a partial
|
|
39
|
+
// workspace build) on its own line: the environment, not app logic.
|
|
40
|
+
failurePatterns: {
|
|
41
|
+
environment: {
|
|
42
|
+
pattern: /\[core\/perps\] environment: cannot resolve /u,
|
|
43
|
+
message: "the Core checkout cannot resolve a module the action imports (a partial workspace build).",
|
|
44
|
+
userAction: "run yarn build in the Core checkout, then retry"
|
|
45
|
+
}
|
|
17
46
|
},
|
|
47
|
+
// The Core gate decides readiness too, so doctor, verify, run and call agree.
|
|
18
48
|
async runtimeStatus(target) {
|
|
19
49
|
const deps = depsCheck(path.resolve(target));
|
|
20
50
|
const block = deps.status === "current" ? coreDependencyBlock(target) : null;
|
|
@@ -27,6 +57,18 @@ const coreSurface = defineAdapter({
|
|
|
27
57
|
deps: deps.status
|
|
28
58
|
};
|
|
29
59
|
}
|
|
60
|
+
if (deps.status === "current") {
|
|
61
|
+
const imported = await coreControllerImport(target);
|
|
62
|
+
if (!imported.ok) {
|
|
63
|
+
return {
|
|
64
|
+
decision: "blocked",
|
|
65
|
+
reasonCode: "CORE_CONTROLLER_IMPORT_FAILED",
|
|
66
|
+
reasons: [`Core is headless; controller actions cannot import the Perps controller: ${imported.reason}`],
|
|
67
|
+
nextAction: imported.userAction ?? coreRebuildAction(target),
|
|
68
|
+
deps: deps.status
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
30
72
|
const ready = deps.status === "current";
|
|
31
73
|
return {
|
|
32
74
|
decision: ready ? "ready" : "install",
|
|
@@ -51,39 +93,9 @@ const coreSurface = defineAdapter({
|
|
|
51
93
|
};
|
|
52
94
|
}
|
|
53
95
|
},
|
|
54
|
-
devServer: {
|
|
55
|
-
label: "dev-server",
|
|
56
|
-
describe: () => "no dev server (headless)",
|
|
57
|
-
stop() {
|
|
58
|
-
return {
|
|
59
|
-
kind: "headless",
|
|
60
|
-
message: "core is headless \u2014 no dev server runs for a core checkout",
|
|
61
|
-
userAction: "use mm-harness verify (readiness) or mm-harness run (execute a recipe)"
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
logSources() {
|
|
66
|
-
return [];
|
|
67
|
-
},
|
|
68
|
-
appLogSource() {
|
|
69
|
-
return null;
|
|
70
|
-
},
|
|
71
|
-
detect: {
|
|
72
|
-
files: (target) => checkoutHasDir(target, "packages/perps-controller") && checkoutHas(target, "yarn.lock")
|
|
73
|
-
},
|
|
74
|
-
hints: {
|
|
75
|
-
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
76
|
-
relaunch: "mm-harness verify",
|
|
77
|
-
runtimeProbeRecovery: (target) => `mm-harness verify --adapter core --target ${shellQuote(path.resolve(target))}`
|
|
78
|
-
},
|
|
79
|
-
launch: async ({ usage }) => usage("core is headless; there is nothing to launch.", "mm-harness verify"),
|
|
80
|
-
actions: coreActions,
|
|
81
|
-
harness: coreHarness,
|
|
82
|
-
runtimeContext: coreRuntimeContext,
|
|
83
96
|
readiness: coreReadiness,
|
|
84
97
|
fixtures: { operations: ["init", "sync", "set"] },
|
|
85
|
-
prepare: corePrepare
|
|
86
|
-
run: coreRun
|
|
98
|
+
prepare: corePrepare
|
|
87
99
|
});
|
|
88
100
|
adapterRegistry.register(coreSurface);
|
|
89
101
|
export {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { colorHumanMessage, EXIT, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import { colorHumanMessage, EXIT, shellQuote, spawnScriptStreaming } from "@farmslot/recipe-cli/harness";
|
|
4
4
|
import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
|
|
5
5
|
import { sameFeatureFlags } from "../../feature-flags.js";
|
|
6
6
|
import { runnerDir } from "../../paths.js";
|
|
7
7
|
import { readExtensionPinnedFlags } from "./feature-flags.js";
|
|
8
|
+
import { releaseArtifactState } from "./runtime-decision.js";
|
|
8
9
|
async function applyExtensionFixture({
|
|
9
10
|
target,
|
|
10
11
|
fixturePath,
|
|
@@ -13,6 +14,64 @@ async function applyExtensionFixture({
|
|
|
13
14
|
sub: action,
|
|
14
15
|
fixtureFlags
|
|
15
16
|
}) {
|
|
17
|
+
const artifact = releaseArtifactState(target);
|
|
18
|
+
if (artifact.status !== "none") {
|
|
19
|
+
const targetArg = `--target ${shellQuote(target)}`;
|
|
20
|
+
const cdpArg = process.env.CDP_PORT ? ` --cdp-port ${shellQuote(process.env.CDP_PORT)}` : "";
|
|
21
|
+
const canonical = path.resolve(fixturePath) === path.resolve(canonicalFixture);
|
|
22
|
+
const fixtureArg = canonical ? "" : ` --fixture ${shellQuote(fixturePath)}`;
|
|
23
|
+
const checkoutAction = `mm-harness runtime-launch --adapter extension ${targetArg}${cdpArg} && mm-harness fixtures ${action} --adapter extension ${targetArg}${fixtureArg}`;
|
|
24
|
+
let message2;
|
|
25
|
+
let artifactAction;
|
|
26
|
+
let alternatives = [];
|
|
27
|
+
if (artifact.status === "invalid") {
|
|
28
|
+
message2 = `The selected release artifact is no longer valid (${artifact.reason ?? "unknown reason"}) and fixtures will not replace it with the checkout build; re-select it with runtime-launch and the same artifact source, or seed the checkout build instead.`;
|
|
29
|
+
artifactAction = checkoutAction;
|
|
30
|
+
} else {
|
|
31
|
+
let archive;
|
|
32
|
+
try {
|
|
33
|
+
const provenance = JSON.parse(fs.readFileSync(artifact.provenancePath, "utf8"));
|
|
34
|
+
if (typeof provenance.source?.path === "string") archive = provenance.source.path;
|
|
35
|
+
} catch {
|
|
36
|
+
archive = void 0;
|
|
37
|
+
}
|
|
38
|
+
const archivePresent = archive !== void 0 && fs.existsSync(archive);
|
|
39
|
+
const reselect = [
|
|
40
|
+
`mm-harness runtime-launch --adapter extension ${targetArg}${cdpArg}`,
|
|
41
|
+
`--artifact-file ${archivePresent ? shellQuote(archive) : "<path-to-the-artifact-zip>"}`,
|
|
42
|
+
`--artifact-version ${shellQuote(artifact.expectedVersion)}`,
|
|
43
|
+
`--artifact-sha256 ${shellQuote(artifact.sha256)}`,
|
|
44
|
+
`--artifact-cache ${shellQuote(path.dirname(path.dirname(path.dirname(artifact.provenancePath))))}`,
|
|
45
|
+
`--chrome-user-data-dir ${shellQuote(artifact.chromeUserDataDir)}`,
|
|
46
|
+
"--reset-profile"
|
|
47
|
+
].join(" ");
|
|
48
|
+
const publish = canonical ? "" : `mm-harness fixtures init --from ${shellQuote(fixturePath)} --force ${targetArg} && `;
|
|
49
|
+
const importWallet = `mm-harness run wallet.import method=ui credential_source=fixture --adapter extension ${targetArg}`;
|
|
50
|
+
artifactAction = `${publish}${reselect} && ${importWallet}`;
|
|
51
|
+
alternatives = [checkoutAction];
|
|
52
|
+
const archiveNote = archivePresent ? "" : `, supplying its archive with --artifact-file (${archive ? `${archive} no longer exists` : "its provenance names no archive"})`;
|
|
53
|
+
const fixtureNote = canonical ? "" : `, after the selected fixture replaces this slot's canonical fixture ${canonicalFixture}, the only one wallet.import reads`;
|
|
54
|
+
message2 = `Release artifact ${artifact.manifestVersion} is selected and fixtures can only seed the checkout build; re-select it with a clean profile and import the fixture wallet${archiveNote}${fixtureNote}, or seed the checkout build instead.`;
|
|
55
|
+
}
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
command: "fixtures",
|
|
60
|
+
action,
|
|
61
|
+
adapter: "extension",
|
|
62
|
+
fixture: fixturePath,
|
|
63
|
+
canonicalFixture,
|
|
64
|
+
status: "fail",
|
|
65
|
+
exitCode: EXIT.runtime,
|
|
66
|
+
error: { code: "FIXTURES_ARTIFACT_ACTIVE", message: message2, userAction: artifactAction, alternatives }
|
|
67
|
+
}, null, 2));
|
|
68
|
+
} else {
|
|
69
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
|
|
70
|
+
Next: ${artifactAction}${alternatives.map((alternative) => `
|
|
71
|
+
Or: ${alternative}`).join("")}`);
|
|
72
|
+
}
|
|
73
|
+
return EXIT.runtime;
|
|
74
|
+
}
|
|
16
75
|
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
17
76
|
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
18
77
|
if (!fs.existsSync(distManifest)) {
|
|
@@ -1,211 +1,24 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
fetchCdpJson
|
|
8
|
-
} from "./browser-cdp.js";
|
|
9
|
-
const DEVICE_ID = "extension";
|
|
10
|
-
const COMMAND_TIMEOUT_MS = 1e4;
|
|
11
|
-
const EXTENSION_TARGET_TYPES = /* @__PURE__ */ new Set([
|
|
12
|
-
"background_page",
|
|
13
|
-
"other",
|
|
14
|
-
"page",
|
|
15
|
-
"service_worker"
|
|
16
|
-
]);
|
|
17
|
-
const { brokerSocketPath, createBrokerClient, createCdpBroker } = brokerModule;
|
|
18
|
-
async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
19
|
-
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
20
|
-
throw new Error("Extension network observation requires a valid CDP port.");
|
|
21
|
-
}
|
|
22
|
-
const targets = await fetchCdpJson(cdpPort);
|
|
23
|
-
const extensionId = extensionIdFromCdpTargets(targets);
|
|
24
|
-
if (!extensionId) {
|
|
25
|
-
throw new Error(
|
|
26
|
-
"Extension CDP browser or loaded extension target is unavailable."
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
const connection = await createBrowserCdpClient(cdpPort);
|
|
30
|
-
let closed = false;
|
|
31
|
-
const brokerRef = {};
|
|
32
|
-
const targetSessions = /* @__PURE__ */ new Map();
|
|
33
|
-
const attaching = /* @__PURE__ */ new Set();
|
|
34
|
-
const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) => connection.send(method, params, sessionId, timeoutMs);
|
|
35
|
-
const relevantTarget = (target) => {
|
|
36
|
-
if (!EXTENSION_TARGET_TYPES.has(String(target.type))) return false;
|
|
37
|
-
try {
|
|
38
|
-
return new URL(String(target.url)).hostname === extensionId;
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
const refreshBrokerSession = () => {
|
|
44
|
-
if (!brokerRef.current) return;
|
|
45
|
-
brokerRef.current.onSessionClose(DEVICE_ID);
|
|
46
|
-
brokerRef.current.onSessionOpen(DEVICE_ID);
|
|
47
|
-
};
|
|
48
|
-
const attachTarget = async (target) => {
|
|
49
|
-
if (closed || !relevantTarget(target) || targetSessions.has(target.targetId) || attaching.has(target.targetId)) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
attaching.add(target.targetId);
|
|
53
|
-
try {
|
|
54
|
-
const result = asRecord(
|
|
55
|
-
await send("Target.attachToTarget", {
|
|
56
|
-
targetId: target.targetId,
|
|
57
|
-
flatten: true
|
|
58
|
-
})
|
|
59
|
-
);
|
|
60
|
-
const sessionId = String(result.sessionId ?? "");
|
|
61
|
-
if (sessionId) {
|
|
62
|
-
targetSessions.set(target.targetId, sessionId);
|
|
63
|
-
refreshBrokerSession();
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (!/already attached/iu.test(String(error))) {
|
|
67
|
-
brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
68
|
-
}
|
|
69
|
-
} finally {
|
|
70
|
-
attaching.delete(target.targetId);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
connection.onMessage((message) => {
|
|
74
|
-
if (message.method === "Target.targetCreated") {
|
|
75
|
-
const target = asBrowserCdpTarget(message.params?.targetInfo);
|
|
76
|
-
if (target) void attachTarget(target);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (message.method === "Target.attachedToTarget") {
|
|
80
|
-
const target = asBrowserCdpTarget(message.params?.targetInfo);
|
|
81
|
-
const sessionId = String(message.params?.sessionId ?? "");
|
|
82
|
-
if (target && sessionId && relevantTarget(target)) {
|
|
83
|
-
targetSessions.set(target.targetId, sessionId);
|
|
84
|
-
refreshBrokerSession();
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (message.method === "Target.detachedFromTarget") {
|
|
89
|
-
const sessionId = String(message.params?.sessionId ?? "");
|
|
90
|
-
for (const [targetId, activeSessionId] of targetSessions) {
|
|
91
|
-
if (activeSessionId !== sessionId) continue;
|
|
92
|
-
targetSessions.delete(targetId);
|
|
93
|
-
refreshBrokerSession();
|
|
94
|
-
}
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (message.sessionId && message.method?.startsWith("Network.")) {
|
|
98
|
-
brokerRef.current?.onCdpEvent(
|
|
99
|
-
DEVICE_ID,
|
|
100
|
-
message.method,
|
|
101
|
-
message.params ?? {}
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
connection.onClose(() => {
|
|
106
|
-
if (!closed) brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
107
|
-
});
|
|
108
|
-
await send("Target.setDiscoverTargets", { discover: true });
|
|
109
|
-
const targetResult = asRecord(await send("Target.getTargets"));
|
|
110
|
-
const targetInfos = Array.isArray(targetResult.targetInfos) ? targetResult.targetInfos : [];
|
|
111
|
-
await Promise.all(
|
|
112
|
-
targetInfos.map(asBrowserCdpTarget).filter((target) => Boolean(target)).map(attachTarget)
|
|
113
|
-
);
|
|
114
|
-
if (targetSessions.size === 0) {
|
|
115
|
-
connection.close();
|
|
116
|
-
throw new Error("Extension CDP exposes no attachable extension targets.");
|
|
117
|
-
}
|
|
118
|
-
const sessions = /* @__PURE__ */ new Map([[DEVICE_ID, { brokerReady: true }]]);
|
|
119
|
-
const socketPath = brokerSocketPath(
|
|
120
|
-
path.join(runtimeDir, "extension-network")
|
|
121
|
-
);
|
|
122
|
-
brokerRef.current = createCdpBroker({
|
|
123
|
-
socketPath,
|
|
124
|
-
sessions,
|
|
125
|
-
async sendCommand(_session, method, params, timeoutMs) {
|
|
126
|
-
const sessionIds = [...new Set(targetSessions.values())];
|
|
127
|
-
if (sessionIds.length === 0) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
"Extension CDP has no active extension target sessions."
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const results = await Promise.allSettled(
|
|
133
|
-
sessionIds.map(
|
|
134
|
-
(sessionId) => send(method, params, sessionId, timeoutMs)
|
|
135
|
-
)
|
|
136
|
-
);
|
|
137
|
-
const successes = results.filter(
|
|
138
|
-
(result) => result.status === "fulfilled"
|
|
139
|
-
);
|
|
140
|
-
if (successes.length === 0) {
|
|
141
|
-
const firstFailure = results.find(
|
|
142
|
-
(result) => result.status === "rejected"
|
|
143
|
-
);
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Extension CDP command failed on every target: ${String(firstFailure?.reason ?? method)}`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
if (successes.length !== results.length) {
|
|
149
|
-
results.forEach((result, index) => {
|
|
150
|
-
if (result.status === "fulfilled") return;
|
|
151
|
-
const failedSession = sessionIds[index];
|
|
152
|
-
for (const [targetId, sessionId] of targetSessions) {
|
|
153
|
-
if (sessionId === failedSession) targetSessions.delete(targetId);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
157
|
-
}
|
|
158
|
-
return successes[0].value;
|
|
159
|
-
},
|
|
160
|
-
requestDiscovery() {
|
|
161
|
-
void send("Target.getTargets").then((result) => {
|
|
162
|
-
const infos = asRecord(result).targetInfos;
|
|
163
|
-
if (!Array.isArray(infos)) return;
|
|
164
|
-
for (const value of infos) {
|
|
165
|
-
const target = asBrowserCdpTarget(value);
|
|
166
|
-
if (target) void attachTarget(target);
|
|
167
|
-
}
|
|
168
|
-
}).catch(() => brokerRef.current?.onSessionClose(DEVICE_ID));
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
brokerRef.current.onSessionOpen(DEVICE_ID);
|
|
172
|
-
await waitForSocket(socketPath);
|
|
173
|
-
const client = await createBrokerClient(
|
|
174
|
-
socketPath,
|
|
175
|
-
DEVICE_ID,
|
|
176
|
-
COMMAND_TIMEOUT_MS
|
|
177
|
-
);
|
|
178
|
-
return {
|
|
179
|
-
start(params) {
|
|
180
|
-
return client.control("capture-start", params, COMMAND_TIMEOUT_MS);
|
|
181
|
-
},
|
|
182
|
-
async end(id) {
|
|
183
|
-
return asRecord(
|
|
184
|
-
await client.control("capture-end", { id }, COMMAND_TIMEOUT_MS)
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
async close() {
|
|
188
|
-
closed = true;
|
|
189
|
-
client.close();
|
|
190
|
-
brokerRef.current?.close();
|
|
191
|
-
connection.close();
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function asRecord(value) {
|
|
196
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
+
import { idFromManifest, resolveExtensionId } from "./extension-id.js";
|
|
4
|
+
async function createExtensionNetworkBackend(target, env, artifactsDir) {
|
|
5
|
+
const cdpPort = extensionCdpPort(env);
|
|
6
|
+
const { extensionId } = await resolveExtensionId(target, { cdpPort });
|
|
7
|
+
return observeExtensionNetwork(cdpPort, artifactsDir, extensionId);
|
|
197
8
|
}
|
|
198
|
-
async function
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
while (Date.now() < deadline) {
|
|
202
|
-
if (existsSync(socketPath)) return;
|
|
203
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
204
|
-
}
|
|
205
|
-
throw new Error("Extension network observer broker did not start.");
|
|
9
|
+
async function createLoadedExtensionNetworkBackend(extensionDir, env, artifactsDir) {
|
|
10
|
+
const extensionId = idFromManifest(path.join(extensionDir, "manifest.json"));
|
|
11
|
+
return observeExtensionNetwork(extensionCdpPort(env), artifactsDir, extensionId);
|
|
206
12
|
}
|
|
207
|
-
|
|
208
|
-
|
|
13
|
+
function observeExtensionNetwork(cdpPort, artifactsDir, extensionId) {
|
|
14
|
+
const { createExtensionNetworkObserver } = createRequire(import.meta.url)(
|
|
15
|
+
"@farmslot/adapter-web/network-observer"
|
|
16
|
+
);
|
|
17
|
+
return createExtensionNetworkObserver({
|
|
18
|
+
cdpPort,
|
|
19
|
+
runtimeDir: artifactsDir,
|
|
20
|
+
...extensionId ? { extensionId } : {}
|
|
21
|
+
});
|
|
209
22
|
}
|
|
210
23
|
function extensionCdpPort(env) {
|
|
211
24
|
const port = Number(
|
|
@@ -218,5 +31,5 @@ function extensionCdpPort(env) {
|
|
|
218
31
|
}
|
|
219
32
|
export {
|
|
220
33
|
createExtensionNetworkBackend,
|
|
221
|
-
|
|
34
|
+
createLoadedExtensionNetworkBackend
|
|
222
35
|
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
verifyExtensionConsoleControl
|
|
10
10
|
} from "./console-capture.js";
|
|
11
11
|
import { emitProofFailure } from "../proof-failure.js";
|
|
12
|
+
import { refuseLockedSession } from "../session-lock.js";
|
|
12
13
|
import { launchedBrowser } from "./launched-browser.js";
|
|
13
14
|
const AUTOLAUNCH_ENV = "METAMASK_RECIPE_EXTENSION_AUTOLAUNCH";
|
|
14
15
|
const extensionRun = {
|
|
@@ -39,8 +40,9 @@ const extensionRun = {
|
|
|
39
40
|
});
|
|
40
41
|
},
|
|
41
42
|
// The proof runtime is checked on the run's own ports (the engine's scope).
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// The Extension browser is always headful: a locked session refuses first.
|
|
44
|
+
runtimeCheck({ target, options, heal, state, json, onRecovery }) {
|
|
45
|
+
return async () => refuseLockedSession(json, options) ?? ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
|
|
44
46
|
},
|
|
45
47
|
launchedBrowser: (target, artifactsDir, cdpPort) => launchedBrowser(target, artifactsDir, cdpPort),
|
|
46
48
|
browserProvenance: (browser) => ({
|
|
@@ -181,7 +183,7 @@ const extensionDiagnostics = {
|
|
|
181
183
|
};
|
|
182
184
|
const extensionObservation = {
|
|
183
185
|
network: {
|
|
184
|
-
backend: async (
|
|
186
|
+
backend: async (target, env, artifactsDir) => (await import("./network-observer.js")).createExtensionNetworkBackend(target, env, artifactsDir),
|
|
185
187
|
actions: true
|
|
186
188
|
},
|
|
187
189
|
performance: {
|