@deeeed/metamask-harness 0.74.0 → 0.75.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 +17 -1
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/dist/action-values.js +15 -0
- package/dist/adapters/core/actions.js +20 -0
- package/dist/adapters/core/live-script-env.js +36 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +5 -1
- package/dist/adapters/extension/action-inputs.js +206 -0
- package/dist/adapters/extension/actions.js +51 -0
- package/dist/adapters/extension/console-capture.js +13 -1
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +9 -2
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +11 -2
- package/dist/adapters/mobile/ui.js +701 -0
- package/dist/adapters/performance/cdp-trace.js +24 -6
- package/dist/adapters/performance/device-platform.js +10 -0
- package/dist/adapters/registry.js +1 -1
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +6 -1
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/launch/device.js +2 -6
- package/dist/commands/launch/extension.js +3 -303
- package/dist/commands/launch/index.js +1 -1
- package/dist/commands/launch/mobile.js +2 -190
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/pin.js +226 -0
- package/dist/commands/run-engine.js +38 -138
- package/dist/commands/run-report.js +4 -48
- package/dist/commands/run.js +6 -48
- package/dist/doctor.js +3 -1
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/heal-bounds.js +3 -2
- package/dist/live-adapter-contract.js +8 -34
- package/dist/manifest.js +3 -5
- package/dist/metamask-action-validation.js +5 -196
- package/dist/mm-harness-cli.js +27 -2
- package/dist/network-observation.js +10 -3
- package/dist/performance-observation.js +15 -22
- package/dist/pr-body/render.js +3 -1
- package/dist/recording-target.js +4 -39
- package/dist/run-diagnostics.js +21 -60
- package/dist/run-recording.js +5 -4
- package/dist/runner.js +20 -368
- package/docs/CONTRIBUTING.md +19 -7
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fs, { constants as fsConstants } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
6
|
import { indexArtifactManifest } from "./artifact-files.js";
|
|
7
7
|
import { importRecipeProtocol, runnerDir } from "./paths.js";
|
|
8
8
|
const gitContexts = /* @__PURE__ */ new Map();
|
|
@@ -156,10 +156,11 @@ function sourceSnapshot(root, adapter, excludedRoots = [], includedRoots = []) {
|
|
|
156
156
|
...pathspecs
|
|
157
157
|
]);
|
|
158
158
|
const head = gitBuffer(git.topLevel, ["rev-parse", "HEAD"]);
|
|
159
|
+
const platformFingerprint = adapter ? getAdapterSurface(adapter).sourceFingerprint : void 0;
|
|
159
160
|
return {
|
|
160
161
|
head: head.toString("utf8").trim() || null,
|
|
161
162
|
status,
|
|
162
|
-
sourceFingerprint:
|
|
163
|
+
sourceFingerprint: platformFingerprint && path.resolve(root) === git.topLevel ? platformFingerprint(root) : nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots, head)
|
|
163
164
|
};
|
|
164
165
|
}
|
|
165
166
|
function nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots, head) {
|
package/dist/heal-bounds.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
4
5
|
import { handleHarness } from "./harness.js";
|
|
5
6
|
import { recipeHarnessPath, recipeRuntimePath } from "./paths.js";
|
|
6
7
|
import { EXIT } from "./commands/shared.js";
|
|
@@ -28,7 +29,7 @@ function overlayPresent(target, adapter) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
function overlayDelegateValid(target, adapter) {
|
|
31
|
-
if (adapter
|
|
32
|
+
if (getAdapterSurface(adapter).headless) return true;
|
|
32
33
|
const pointer = path.join(recipeHarnessPath(target, adapter), "runner", ".runner-source");
|
|
33
34
|
if (!fs.existsSync(pointer)) return true;
|
|
34
35
|
const runnerPath = fs.readFileSync(pointer, "utf8").trim();
|
|
@@ -36,7 +37,7 @@ function overlayDelegateValid(target, adapter) {
|
|
|
36
37
|
return fs.existsSync(path.join(runnerPath, "bin", "mm-harness"));
|
|
37
38
|
}
|
|
38
39
|
async function ensureOverlay(adapter, target, heal, state, json) {
|
|
39
|
-
if (adapter
|
|
40
|
+
if (getAdapterSurface(adapter).headless) return { ok: true };
|
|
40
41
|
if (overlayPresent(target, adapter) && overlayDelegateValid(target, adapter)) return { ok: true };
|
|
41
42
|
if (heal === "off") return { ok: true };
|
|
42
43
|
const installBin = process.env.MM_HARNESS_INSTALL_BIN;
|
|
@@ -10,7 +10,8 @@ import { pathToFileURL } from "node:url";
|
|
|
10
10
|
import { build } from "esbuild";
|
|
11
11
|
import { init, parse } from "es-module-lexer";
|
|
12
12
|
import { resolveRequiredLocalProtocolRoot, runnerDir } from "./paths.js";
|
|
13
|
-
import {
|
|
13
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
14
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
14
15
|
import { serializeTrustedMutationContext } from "./funding-execution-context.js";
|
|
15
16
|
function actionFileStem(action) {
|
|
16
17
|
return String(action).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
@@ -213,8 +214,11 @@ function bindImportMetaUrls() {
|
|
|
213
214
|
}
|
|
214
215
|
};
|
|
215
216
|
}
|
|
217
|
+
function tsxLiveScripts(platform) {
|
|
218
|
+
return platform && adapterRegistry.has(platform) ? getAdapterSurface(platform).actions.tsxLiveScripts : void 0;
|
|
219
|
+
}
|
|
216
220
|
function commandFor(file, projectRoot, platform) {
|
|
217
|
-
const needsTsx = platform
|
|
221
|
+
const needsTsx = Boolean(tsxLiveScripts(platform)) || importsSourceTypescript(file);
|
|
218
222
|
if (!needsTsx) {
|
|
219
223
|
return { command: process.execPath, args: [file] };
|
|
220
224
|
}
|
|
@@ -268,36 +272,6 @@ function importsSourceTypescriptFrom(file, visited) {
|
|
|
268
272
|
async function resolveLiveAdapter(platform, action, runtimeEnv) {
|
|
269
273
|
return firstExecutablePath(candidatePaths(platform, action, runtimeEnv));
|
|
270
274
|
}
|
|
271
|
-
const CORE_WORKSPACE_PACKAGES = [
|
|
272
|
-
"@metamask/base-controller",
|
|
273
|
-
"@metamask/messenger",
|
|
274
|
-
"@metamask/controller-utils",
|
|
275
|
-
"@metamask/keyring-controller"
|
|
276
|
-
];
|
|
277
|
-
async function platformAdapterEnv(platform, projectRoot, tempDir) {
|
|
278
|
-
if (platform !== "core") return {};
|
|
279
|
-
const paths = {};
|
|
280
|
-
for (const pkg of CORE_WORKSPACE_PACKAGES) {
|
|
281
|
-
const packageDir = path.join(projectRoot, "packages", pkg.replace("@metamask/", ""));
|
|
282
|
-
const distEntry = path.join(packageDir, "dist/index.cjs");
|
|
283
|
-
if (existsSync(distEntry)) continue;
|
|
284
|
-
const srcDir = path.join(packageDir, "src");
|
|
285
|
-
if (!existsSync(path.join(srcDir, "index.ts"))) continue;
|
|
286
|
-
paths[pkg] = [path.join(srcDir, "index.ts")];
|
|
287
|
-
paths[`${pkg}/*`] = [path.join(srcDir, "*")];
|
|
288
|
-
}
|
|
289
|
-
if (Object.keys(paths).length === 0) return {};
|
|
290
|
-
const tsconfigPath = path.join(tempDir, "core-adapter.tsconfig.json");
|
|
291
|
-
await writeFile(
|
|
292
|
-
tsconfigPath,
|
|
293
|
-
`${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
|
|
294
|
-
`
|
|
295
|
-
);
|
|
296
|
-
return {
|
|
297
|
-
TSX_TSCONFIG_PATH: tsconfigPath,
|
|
298
|
-
NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
275
|
async function runLiveAdapterScript({ platform, action, node, context, prepared }) {
|
|
302
276
|
const script = prepared?.entryPath ?? await resolveLiveAdapter(platform, action, context?.env);
|
|
303
277
|
if (!script) return null;
|
|
@@ -320,7 +294,7 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
320
294
|
await writeFile(inputPath, `${JSON.stringify(input, null, 2)}
|
|
321
295
|
`);
|
|
322
296
|
const command = prepared ? commandForPrepared(context.projectRoot, platform) : commandFor(script, context.projectRoot, platform);
|
|
323
|
-
const platformEnv = await
|
|
297
|
+
const platformEnv = await tsxLiveScripts(platform)?.env(context.projectRoot, tempDir) ?? {};
|
|
324
298
|
const processTimeoutMs = liveAdapterProcessTimeoutMs(node);
|
|
325
299
|
const result = await runProcess(command.command, [...command.args, inputPath], {
|
|
326
300
|
cwd: context.projectRoot,
|
|
@@ -380,7 +354,7 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
380
354
|
return 6e4 + settleAllowance;
|
|
381
355
|
}
|
|
382
356
|
function commandForPrepared(projectRoot, platform) {
|
|
383
|
-
if (platform
|
|
357
|
+
if (!tsxLiveScripts(platform)) {
|
|
384
358
|
return { command: process.execPath, args: ["--input-type=module", "-"] };
|
|
385
359
|
}
|
|
386
360
|
const tsxBin = resolveTsxBin(projectRoot);
|
package/dist/manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
4
5
|
import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
|
|
5
6
|
function loadMetaMaskMobileActionManifest() {
|
|
6
7
|
return asActionManifest(readJson(manifestPath("mobile")));
|
|
@@ -16,10 +17,7 @@ function loadMetaMaskTerminalActionManifest() {
|
|
|
16
17
|
}
|
|
17
18
|
function loadActionManifest(adapter, overridePath) {
|
|
18
19
|
if (overridePath) return asActionManifest(readJson(path.resolve(overridePath)));
|
|
19
|
-
|
|
20
|
-
if (adapter === "core") return loadMetaMaskCoreActionManifest();
|
|
21
|
-
if (adapter === "terminal") return loadMetaMaskTerminalActionManifest();
|
|
22
|
-
return loadMetaMaskExtensionActionManifest();
|
|
20
|
+
return asActionManifest(readJson(getAdapterSurface(adapter).actions.manifestPath()));
|
|
23
21
|
}
|
|
24
22
|
async function resolveActionManifest(adapter, overridePath, librarySources, taskActionRoot) {
|
|
25
23
|
if (overridePath) {
|
|
@@ -40,7 +38,7 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
|
|
|
40
38
|
})
|
|
41
39
|
};
|
|
42
40
|
}
|
|
43
|
-
const canonicalPath = path.resolve(
|
|
41
|
+
const canonicalPath = path.resolve(getAdapterSurface(adapter).actions.manifestPath());
|
|
44
42
|
const canonicalRoot = path.dirname(path.dirname(canonicalPath));
|
|
45
43
|
const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: canonicalRoot }];
|
|
46
44
|
const manifests = [];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
2
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
1
3
|
function isRecord(value) {
|
|
2
4
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
5
|
}
|
|
@@ -22,208 +24,15 @@ function validateMetaMaskActionInputs(recipe, adapter, params) {
|
|
|
22
24
|
if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
|
|
23
25
|
const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
|
|
24
26
|
if (!nodes) return [];
|
|
27
|
+
const surface = adapter && adapterRegistry.has(adapter) ? getAdapterSurface(adapter) : void 0;
|
|
25
28
|
const findings = [];
|
|
26
29
|
for (const [nodeId, rawNode] of Object.entries(nodes)) {
|
|
27
30
|
const node = params ? resolveMetaMaskParameterValue(rawNode, params) : rawNode;
|
|
28
31
|
if (!isRecord(node)) continue;
|
|
29
32
|
const action = node.action;
|
|
30
|
-
|
|
31
|
-
if (typeof node[name] !== "number" || node[name] <= limit) return;
|
|
32
|
-
findings.push({
|
|
33
|
-
severity: "error",
|
|
34
|
-
code: "recipe.invalid_param",
|
|
35
|
-
path: `workflow.nodes.${nodeId}.${name}`,
|
|
36
|
-
message: `${action} ${name} must not exceed ${limit}`
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
if (action === "metamask.perps.read_snapshot" && adapter === "extension") {
|
|
40
|
-
if (node.market !== void 0 && node.markets !== void 0) {
|
|
41
|
-
findings.push({
|
|
42
|
-
severity: "error",
|
|
43
|
-
code: "recipe.invalid_param",
|
|
44
|
-
path: `workflow.nodes.${nodeId}.markets`,
|
|
45
|
-
message: `${action} accepts market or markets, not both`
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (isRecord(node.history)) {
|
|
49
|
-
const start = node.history.start_time_ms;
|
|
50
|
-
const end = node.history.end_time_ms;
|
|
51
|
-
if (typeof start === "number" && typeof end === "number" && start > end) {
|
|
52
|
-
findings.push({
|
|
53
|
-
severity: "error",
|
|
54
|
-
code: "recipe.invalid_param",
|
|
55
|
-
path: `workflow.nodes.${nodeId}.history.start_time_ms`,
|
|
56
|
-
message: `${action} history.start_time_ms must not exceed history.end_time_ms`
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
if (typeof node.history.limit === "number" && node.history.limit > 500) {
|
|
60
|
-
findings.push({
|
|
61
|
-
severity: "error",
|
|
62
|
-
code: "recipe.invalid_param",
|
|
63
|
-
path: `workflow.nodes.${nodeId}.history.limit`,
|
|
64
|
-
message: `${action} history.limit must not exceed 500`
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (typeof node.max_items_per_section === "number" && node.max_items_per_section > 500) {
|
|
69
|
-
findings.push({
|
|
70
|
-
severity: "error",
|
|
71
|
-
code: "recipe.invalid_param",
|
|
72
|
-
path: `workflow.nodes.${nodeId}.max_items_per_section`,
|
|
73
|
-
message: `${action} max_items_per_section must not exceed 500`
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
if (typeof node.timeout_ms === "number" && node.timeout_ms > 6e4) {
|
|
77
|
-
findings.push({
|
|
78
|
-
severity: "error",
|
|
79
|
-
code: "recipe.invalid_param",
|
|
80
|
-
path: `workflow.nodes.${nodeId}.timeout_ms`,
|
|
81
|
-
message: `${action} timeout_ms must not exceed 60000`
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (isRecord(node.sampling)) {
|
|
85
|
-
if (typeof node.sampling.count === "number" && node.sampling.count > 10) {
|
|
86
|
-
findings.push({
|
|
87
|
-
severity: "error",
|
|
88
|
-
code: "recipe.invalid_param",
|
|
89
|
-
path: `workflow.nodes.${nodeId}.sampling.count`,
|
|
90
|
-
message: `${action} sampling.count must not exceed 10`
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
if (typeof node.sampling.interval_ms === "number" && node.sampling.interval_ms > 3e4) {
|
|
94
|
-
findings.push({
|
|
95
|
-
severity: "error",
|
|
96
|
-
code: "recipe.invalid_param",
|
|
97
|
-
path: `workflow.nodes.${nodeId}.sampling.interval_ms`,
|
|
98
|
-
message: `${action} sampling.interval_ms must not exceed 30000`
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (isRecord(node.expect)) {
|
|
103
|
-
if (typeof node.expect.minimum_changed_market_count === "number" && node.expect.minimum_changed_market_count > 100) {
|
|
104
|
-
findings.push({
|
|
105
|
-
severity: "error",
|
|
106
|
-
code: "recipe.invalid_param",
|
|
107
|
-
path: `workflow.nodes.${nodeId}.expect.minimum_changed_market_count`,
|
|
108
|
-
message: `${action} expect.minimum_changed_market_count must not exceed 100`
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
if (typeof node.expect.minimum_history_count === "number" && node.expect.minimum_history_count > 500) {
|
|
112
|
-
findings.push({
|
|
113
|
-
severity: "error",
|
|
114
|
-
code: "recipe.invalid_param",
|
|
115
|
-
path: `workflow.nodes.${nodeId}.expect.minimum_history_count`,
|
|
116
|
-
message: `${action} expect.minimum_history_count must not exceed 500`
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (action === "metamask.perps.assert_visible_consistency" && adapter === "extension") {
|
|
122
|
-
maximum("sample_count", 10);
|
|
123
|
-
maximum("sample_interval_ms", 3e4);
|
|
124
|
-
maximum("minimum_changed_count", 200);
|
|
125
|
-
maximum("minimum_recent_activity_count", 50);
|
|
126
|
-
maximum("timeout_ms", 12e4);
|
|
127
|
-
maximum("max_items", 200);
|
|
128
|
-
}
|
|
129
|
-
const fundedVisibleMutation = adapter === "extension" && (action === "metamask.perps.close_visible_position" || action === "metamask.perps.close_orders" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl" || action === "metamask.perps.place_order" && node.confirm_live === true);
|
|
130
|
-
if (fundedVisibleMutation) {
|
|
131
|
-
const required = (name, pattern, message) => {
|
|
132
|
-
if (typeof node[name] === "string" && pattern.test(node[name])) return;
|
|
133
|
-
findings.push({
|
|
134
|
-
severity: "error",
|
|
135
|
-
code: "recipe.invalid_param",
|
|
136
|
-
path: `workflow.nodes.${nodeId}.${name}`,
|
|
137
|
-
message
|
|
138
|
-
});
|
|
139
|
-
};
|
|
140
|
-
const decimal = action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? node.funding_amount : node.amount ?? node.notional;
|
|
141
|
-
const decimalText = String(decimal ?? "");
|
|
142
|
-
const match = /^([1-9][0-9]*)(?:\.([0-9]{1,6}))?$/u.exec(decimalText);
|
|
143
|
-
if (!match) {
|
|
144
|
-
findings.push({
|
|
145
|
-
severity: "error",
|
|
146
|
-
code: "recipe.invalid_param",
|
|
147
|
-
path: `workflow.nodes.${nodeId}.${action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? "funding_amount" : "amount"}`,
|
|
148
|
-
message: `${action} funded decimal amount must be positive plain decimal text with at most six places`
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (action === "metamask.perps.place_order" && node.submit_attempts !== void 0 && node.submit_attempts !== 1) {
|
|
152
|
-
findings.push({
|
|
153
|
-
severity: "error",
|
|
154
|
-
code: "recipe.invalid_param",
|
|
155
|
-
path: `workflow.nodes.${nodeId}.submit_attempts`,
|
|
156
|
-
message: `${action} funded visible submission requires submit_attempts=1`
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
if (action === "metamask.perps.close_visible_position" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
160
|
-
required("funding_side", /^(?:long|short)$/u, `${action} requires the authorized position side`);
|
|
161
|
-
}
|
|
162
|
-
if (action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
163
|
-
required(
|
|
164
|
-
"funding_position_size",
|
|
165
|
-
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
166
|
-
`${action} requires the exact visible position size`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
if (action === "metamask.perps.close_visible_position" && node.cleanup_only === true) {
|
|
170
|
-
required(
|
|
171
|
-
"funding_position_size",
|
|
172
|
-
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
173
|
-
`${action} cleanup requires the exact visible position size`
|
|
174
|
-
);
|
|
175
|
-
if (node.amount != null) {
|
|
176
|
-
findings.push({
|
|
177
|
-
severity: "error",
|
|
178
|
-
code: "recipe.invalid_param",
|
|
179
|
-
path: `workflow.nodes.${nodeId}.amount`,
|
|
180
|
-
message: `${action} cleanup must close the full exact position`
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (action === "metamask.perps.edit_margin" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.funding_amount ?? ""))) {
|
|
185
|
-
findings.push({
|
|
186
|
-
severity: "error",
|
|
187
|
-
code: "recipe.invalid_param",
|
|
188
|
-
path: `workflow.nodes.${nodeId}.funding_amount`,
|
|
189
|
-
message: `${action} requires the authorized position amount`
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
if (action === "metamask.perps.close_orders") {
|
|
193
|
-
required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
|
|
194
|
-
const outputPrice = /^\{\{outputs\.[A-Za-z0-9_.-]+\}\}$/u.test(String(node.limit_price ?? ""));
|
|
195
|
-
if (!outputPrice && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
196
|
-
findings.push({
|
|
197
|
-
severity: "error",
|
|
198
|
-
code: "recipe.invalid_param",
|
|
199
|
-
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
200
|
-
message: `${action} requires an explicit positive limit_price for resumable cancellation`
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
205
|
-
findings.push({
|
|
206
|
-
severity: "error",
|
|
207
|
-
code: "recipe.invalid_param",
|
|
208
|
-
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
209
|
-
message: `${action} funded limit submission requires explicit limit_price; offset-only pricing is not resumable`
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
for (const name of ["identity_rounding_tolerance_base_units", "balance_reconciliation_tolerance_base_units"]) {
|
|
213
|
-
if (node[name] === void 0) continue;
|
|
214
|
-
const value = String(node[name]);
|
|
215
|
-
if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(value) || BigInt(value) > 1000000n) {
|
|
216
|
-
findings.push({
|
|
217
|
-
severity: "error",
|
|
218
|
-
code: "recipe.invalid_param",
|
|
219
|
-
path: `workflow.nodes.${nodeId}.${name}`,
|
|
220
|
-
message: `${action} ${name} must be an integer string from 0 through 1000000`
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
33
|
+
findings.push(...surface?.actions.inputFindings?.(nodeId, node) ?? []);
|
|
225
34
|
const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
|
|
226
|
-
const createsOrder = action === "metamask.perps.place_order" ||
|
|
35
|
+
const createsOrder = action === "metamask.perps.place_order" || !surface?.actions.ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
227
36
|
if (!createsOrder) continue;
|
|
228
37
|
const requireOneOf = (names, message) => {
|
|
229
38
|
const present = names.some((name) => {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { color } from "./cli-color.js";
|
|
|
7
7
|
import { withCommandJournal } from "./command-journal.js";
|
|
8
8
|
import { JsonStreamWriter } from "./json-stream.js";
|
|
9
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
10
|
+
import { handlePin } from "./commands/pin.js";
|
|
10
11
|
import { handleCallHelp } from "./commands/call.js";
|
|
11
12
|
import { handleHelp } from "./commands/help.js";
|
|
12
13
|
import { handleTutorial } from "./commands/tutorial.js";
|
|
@@ -834,6 +835,27 @@ Example:
|
|
|
834
835
|
Example:
|
|
835
836
|
mm-harness reload`
|
|
836
837
|
},
|
|
838
|
+
{
|
|
839
|
+
name: "pin",
|
|
840
|
+
summary: "Copy this installed mm-harness into an immutable, versioned store and print the copy for a task to run, so a later global upgrade cannot change a running task.",
|
|
841
|
+
example: "mm-harness pin",
|
|
842
|
+
helpText: `mm-harness pin [flags]
|
|
843
|
+
|
|
844
|
+
Copies the installed mm-harness running this command into an immutable,
|
|
845
|
+
versioned store (~/.local/share/mm-harness/pins/<version>-<hash>/; APFS clone
|
|
846
|
+
where available) and prints the copy's executable. A task records that path,
|
|
847
|
+
so \`npm i -g\` upgrading the global install never changes the harness a running
|
|
848
|
+
task runs. Idempotent: the same install returns the same pin. A source checkout
|
|
849
|
+
(MM_HARNESS_BIN) or an existing pin is printed as is.
|
|
850
|
+
|
|
851
|
+
--json Machine-readable { version, source, executable, pinned, created }
|
|
852
|
+
|
|
853
|
+
MM_HARNESS_PIN_ROOT overrides the store location.
|
|
854
|
+
|
|
855
|
+
Example:
|
|
856
|
+
mm-harness pin
|
|
857
|
+
mm-harness pin --json`
|
|
858
|
+
},
|
|
837
859
|
{
|
|
838
860
|
name: "update",
|
|
839
861
|
summary: "Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).",
|
|
@@ -980,7 +1002,7 @@ const HELP_GROUPS = [
|
|
|
980
1002
|
{
|
|
981
1003
|
title: "MAINTAIN",
|
|
982
1004
|
blurb: "keep the installed harness current with the npm registry",
|
|
983
|
-
commands: ["setup-base", "update"]
|
|
1005
|
+
commands: ["setup-base", "update", "pin"]
|
|
984
1006
|
}
|
|
985
1007
|
];
|
|
986
1008
|
function commandMeta(name) {
|
|
@@ -1088,6 +1110,9 @@ for (const command of REAL) {
|
|
|
1088
1110
|
if (command.name === "update") {
|
|
1089
1111
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
1090
1112
|
}
|
|
1113
|
+
if (command.name === "pin") {
|
|
1114
|
+
process.exit(handlePin(rawArgv.slice(1)));
|
|
1115
|
+
}
|
|
1091
1116
|
if (command.name === "help") {
|
|
1092
1117
|
process.exitCode = handleHelp(rawArgv.slice(1), { packageRoot, harnessVersion: pkgVersion });
|
|
1093
1118
|
return;
|
|
@@ -1116,7 +1141,7 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
1116
1141
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
1117
1142
|
process.exit(result.status ?? 1);
|
|
1118
1143
|
});
|
|
1119
|
-
const NUDGE_SKIP = ["help", "tutorial", "setup-base", "update", "completions", "completion-candidates"];
|
|
1144
|
+
const NUDGE_SKIP = ["help", "tutorial", "setup-base", "update", "pin", "completions", "completion-candidates"];
|
|
1120
1145
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
1121
1146
|
maybeNudge();
|
|
1122
1147
|
}
|
|
@@ -4,6 +4,7 @@ import configModule from "../adapters/mobile/bridge-runtime/lib/config.cjs";
|
|
|
4
4
|
import {
|
|
5
5
|
createExtensionNetworkObserver
|
|
6
6
|
} from "./adapters/extension/network-observer.js";
|
|
7
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
7
8
|
import {
|
|
8
9
|
indexArtifactManifest,
|
|
9
10
|
writeContainedArtifact
|
|
@@ -14,7 +15,8 @@ const { brokerSocketPath, createBrokerClient } = brokerModule;
|
|
|
14
15
|
const { resolvePort } = configModule;
|
|
15
16
|
const sessions = /* @__PURE__ */ new Map();
|
|
16
17
|
async function startRunNetworkObservation(adapter, target, artifactsDir, env, ports = {}) {
|
|
17
|
-
|
|
18
|
+
const network = getAdapterSurface(adapter).observation?.network;
|
|
19
|
+
if (!network) return void 0;
|
|
18
20
|
const runtimeEnv = {
|
|
19
21
|
...env,
|
|
20
22
|
...ports.cdpPort ? { CDP_PORT: ports.cdpPort, RECIPE_CDP_PORT: ports.cdpPort } : {},
|
|
@@ -29,7 +31,7 @@ async function startRunNetworkObservation(adapter, target, artifactsDir, env, po
|
|
|
29
31
|
};
|
|
30
32
|
sessions.set(key, session);
|
|
31
33
|
try {
|
|
32
|
-
session.backend =
|
|
34
|
+
session.backend = await network.backend(target, runtimeEnv, key);
|
|
33
35
|
} catch (error) {
|
|
34
36
|
session.setupError = boundedError(error);
|
|
35
37
|
}
|
|
@@ -76,7 +78,7 @@ async function startRunNetworkObservation(adapter, target, artifactsDir, env, po
|
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
async function handleRunNetworkAction(platform, action, node, context) {
|
|
79
|
-
if (platform
|
|
81
|
+
if (!getAdapterSurface(platform).observation?.network?.actions) return null;
|
|
80
82
|
if (action === "app.network_assert") {
|
|
81
83
|
const assertion = await import("../library/actions/shared/app/network-assert.mjs");
|
|
82
84
|
return {
|
|
@@ -261,6 +263,9 @@ async function indexArtifact(artifactManifestPath, relativePath, label) {
|
|
|
261
263
|
]
|
|
262
264
|
);
|
|
263
265
|
}
|
|
266
|
+
async function createExtensionNetworkBackend(env, artifactsDir) {
|
|
267
|
+
return createExtensionNetworkObserver(extensionCdpPort(env), artifactsDir);
|
|
268
|
+
}
|
|
264
269
|
function extensionCdpPort(env) {
|
|
265
270
|
const port = Number(
|
|
266
271
|
env.CDP_PORT ?? env.RECIPE_CDP_PORT ?? process.env.CDP_PORT
|
|
@@ -278,6 +283,8 @@ function asRecord(value) {
|
|
|
278
283
|
}
|
|
279
284
|
export {
|
|
280
285
|
connectMobileBroker,
|
|
286
|
+
createExtensionNetworkBackend,
|
|
287
|
+
createMobileNetworkBackend,
|
|
281
288
|
handleRunNetworkAction,
|
|
282
289
|
startRunNetworkObservation
|
|
283
290
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { gzip } from "node:zlib";
|
|
4
|
-
import {
|
|
5
|
-
createMobilePerformanceBackend
|
|
6
|
-
} from "./adapters/mobile/performance-observer.js";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
7
5
|
import { createExtensionPerformanceBackend } from "./adapters/extension/performance-observer.js";
|
|
6
|
+
import { TRACE_KINDS } from "./adapters/performance/cdp-trace.js";
|
|
8
7
|
import { summarizeFrames } from "./adapters/mobile/frame-metrics.js";
|
|
9
8
|
import { summarizeJavaScriptTasks } from "./adapters/performance/js-task-metrics.js";
|
|
10
9
|
import {
|
|
@@ -17,10 +16,11 @@ const DEFAULT_MAX_DURATION_MS = 5 * 60 * 1e3;
|
|
|
17
16
|
const MAX_DURATION_MS = 60 * 60 * 1e3;
|
|
18
17
|
const gzipAsync = promisify(gzip);
|
|
19
18
|
function startRunPerformanceObservation(adapter, target, artifactsDir, env, ports = {}) {
|
|
20
|
-
|
|
19
|
+
const observer = getAdapterSurface(adapter).observation?.performance;
|
|
20
|
+
if (!observer) return void 0;
|
|
21
21
|
const key = path.resolve(artifactsDir);
|
|
22
22
|
const session = {
|
|
23
|
-
|
|
23
|
+
observer,
|
|
24
24
|
artifactsDir: key,
|
|
25
25
|
env: {
|
|
26
26
|
...env,
|
|
@@ -84,7 +84,7 @@ function startRunPerformanceObservation(adapter, target, artifactsDir, env, port
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
async function handleRunPerformanceAction(platform, action, node, context) {
|
|
87
|
-
if (platform
|
|
87
|
+
if (!getAdapterSurface(platform).observation?.performance) return null;
|
|
88
88
|
if (action === "app.performance_assert") {
|
|
89
89
|
return { output: await assertPerformance(node, context) };
|
|
90
90
|
}
|
|
@@ -154,10 +154,7 @@ async function performanceBackend(session) {
|
|
|
154
154
|
if (session.backend) return session.backend;
|
|
155
155
|
if (session.backendError) throw new Error(session.backendError);
|
|
156
156
|
try {
|
|
157
|
-
session.backend = session.
|
|
158
|
-
extensionCdpPort(session),
|
|
159
|
-
session.target
|
|
160
|
-
);
|
|
157
|
+
session.backend = await session.observer.backend(session.target, session.env, session.ports);
|
|
161
158
|
return session.backend;
|
|
162
159
|
} catch (error) {
|
|
163
160
|
session.backendError = errorMessage(error);
|
|
@@ -182,7 +179,7 @@ function finishCaptureWindow(session, window) {
|
|
|
182
179
|
return window.result;
|
|
183
180
|
}
|
|
184
181
|
function buildPerformanceArtifact(window, endedAtEpochMs, result, allEvents) {
|
|
185
|
-
const javascriptTasksApplicable = result.platform
|
|
182
|
+
const javascriptTasksApplicable = TRACE_KINDS[result.platform].javascriptTasks;
|
|
186
183
|
const javascript = javascriptTasksApplicable ? result.javascript : {
|
|
187
184
|
...result.javascript,
|
|
188
185
|
applicability: "not_applicable",
|
|
@@ -436,7 +433,7 @@ function chromeTracePath(jsonPath) {
|
|
|
436
433
|
return jsonPath.endsWith(".json") ? `${jsonPath.slice(0, -".json".length)}-trace.json.gz` : `${jsonPath}-trace.json.gz`;
|
|
437
434
|
}
|
|
438
435
|
function renderPerformanceHtml(artifact) {
|
|
439
|
-
const javascriptTasksApplicable = artifact.platform
|
|
436
|
+
const javascriptTasksApplicable = TRACE_KINDS[artifact.platform].javascriptTasks;
|
|
440
437
|
const iosCadence = artifact.platform === "ios";
|
|
441
438
|
const maxNodeDurationMs = Math.max(
|
|
442
439
|
1,
|
|
@@ -503,7 +500,7 @@ function unavailableCapture(session, window, reason) {
|
|
|
503
500
|
window,
|
|
504
501
|
Date.now(),
|
|
505
502
|
{
|
|
506
|
-
platform:
|
|
503
|
+
platform: session.observer.platform(session.env),
|
|
507
504
|
javascript: unavailableJavaScript,
|
|
508
505
|
nativeUi: unavailableNativeUi,
|
|
509
506
|
trace: {
|
|
@@ -520,17 +517,12 @@ function unavailableCapture(session, window, reason) {
|
|
|
520
517
|
session.nodeEvents
|
|
521
518
|
);
|
|
522
519
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
const explicit = String(
|
|
526
|
-
session.env.MM_HARNESS_EXPLICIT_PLATFORM ?? session.env.RECIPE_HARNESS_PLATFORM ?? ""
|
|
527
|
-
).toLowerCase();
|
|
528
|
-
if (explicit === "android" || explicit === "ios") return explicit;
|
|
529
|
-
return session.env.ADB_SERIAL || session.env.ANDROID_SERIAL || session.env.ANDROID_DEVICE ? "android" : "ios";
|
|
520
|
+
async function createExtensionPerformanceObserver(target, env, ports) {
|
|
521
|
+
return createExtensionPerformanceBackend(extensionCdpPort(ports, env), target);
|
|
530
522
|
}
|
|
531
|
-
function extensionCdpPort(
|
|
523
|
+
function extensionCdpPort(ports, env) {
|
|
532
524
|
const port = Number(
|
|
533
|
-
|
|
525
|
+
ports.cdpPort ?? env.CDP_PORT ?? env.RECIPE_CDP_PORT
|
|
534
526
|
);
|
|
535
527
|
if (!Number.isInteger(port) || port <= 0) {
|
|
536
528
|
throw new Error("Extension performance observation requires CDP_PORT.");
|
|
@@ -572,6 +564,7 @@ function errorMessage(error) {
|
|
|
572
564
|
}
|
|
573
565
|
export {
|
|
574
566
|
buildPerformanceArtifact,
|
|
567
|
+
createExtensionPerformanceObserver,
|
|
575
568
|
handleRunPerformanceAction,
|
|
576
569
|
renderPerformanceHtml,
|
|
577
570
|
startRunPerformanceObservation
|
package/dist/pr-body/render.js
CHANGED
|
@@ -37,6 +37,8 @@ function countRecipeNodes(recipe) {
|
|
|
37
37
|
if (Array.isArray(record.steps)) return record.steps.length;
|
|
38
38
|
const workflow = record.workflow;
|
|
39
39
|
if (!workflow || typeof workflow !== "object") return 0;
|
|
40
|
+
const graph = workflow.nodes;
|
|
41
|
+
if (graph && typeof graph === "object" && !Array.isArray(graph)) return Object.keys(graph).length;
|
|
40
42
|
return Object.values(workflow).reduce(
|
|
41
43
|
(total, value) => total + (Array.isArray(value) ? value.length : 0),
|
|
42
44
|
0
|
|
@@ -47,7 +49,7 @@ function recipeSection(file) {
|
|
|
47
49
|
const nodes = countRecipeNodes(recipe);
|
|
48
50
|
const record = recipe && typeof recipe === "object" ? recipe : {};
|
|
49
51
|
const title = typeof record.title === "string" ? record.title : typeof record.description === "string" ? record.description : "";
|
|
50
|
-
const summary = escapeHtml(`recipe.json (${nodes}
|
|
52
|
+
const summary = escapeHtml(`recipe.json (${nodes} nodes${title ? ` \u2014 ${title.slice(0, 120)}` : ""})`);
|
|
51
53
|
return {
|
|
52
54
|
nodes,
|
|
53
55
|
body: [`<details><summary>${summary}</summary>`, "", fenced(JSON.stringify(recipe, null, 2), "json"), "</details>"].join("\n")
|