@deeeed/metamask-harness 0.73.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 +22 -0
- 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 +8 -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 +17 -1
- 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 +19 -1
- 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 +17 -0
- package/dist/adapters/surface.js +6 -11
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/launch.js +85 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +9 -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 +342 -0
- package/dist/commands/launch/index.js +9 -942
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/pin.js +226 -0
- package/dist/commands/review.js +3 -1
- 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/commands/shared.js +2 -2
- 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 +20 -3
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.75.0 - 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `mm-harness pin` gives a task a harness that a global upgrade cannot change. It copies the running installed harness into an immutable, versioned store (`~/.local/share/mm-harness/pins/<version>-<hash>/`, APFS clone where available; `MM_HARNESS_PIN_ROOT` overrides) and prints the copy's executable for the task to record. Pins are keyed by the install's full content, so the same install returns the same pin and a same-version rebuild gets its own; absolute links into the install are re-pointed into the copy, and a pin whose content changed after it was created is refused rather than reused. A source checkout or an existing pin is printed as is; an install whose dependencies or links lead outside it is refused. `doctor` reports a pinned runner as `pinned-install`.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `run` and `call` reach platform behavior through the adapter surface instead of branching on the adapter name: new optional `AdapterSurface` members `run` (environment, runtime preparation and checks, dependency block, heal wording, executed browser), `recording`, `diagnostics` (console capture, wallet log, in-app issue buffer), `observation` (network and performance) and `sourceFingerprint`. The engine modules (`run-engine`, `run`, `call`, `run-report`, `run-recording`, `recording-target`, `heal-bounds`, `execution-provenance`, `run-diagnostics`, performance and network observation, `cdp-trace`) go from 48 adapter-name branches to 0. Each platform's implementation lives under `src/adapters/<adapter>/` (`run.ts`, and now `launch.ts` for the Extension and Mobile launch profiles). The test-only `example` adapter is reached by the run/call engine with no shared-file edit. Flags, output, and the CLI goldens are unchanged, with one ordering exception: for `run` and `call` on Mobile, an invalid release-artifact state file is now reported after the running-recipe check instead of before it.
|
|
14
|
+
- The runner's action set comes from a required `AdapterSurface.actions` member instead of adapter-name branches: the bundled manifest, the semantic actions each platform bundles, when live adapter scripts run and under tsx, the `cdp.target` probe, the `ui.*` transport, wrappers around the action and `app.lifecycle` adapters, funded-mutation binding and recipe input checks. The device video recorder joins `AdapterSurface.recording`. `adapters.ts`, `runner.ts`, `metamask-action-validation.ts`, `live-adapter-contract.ts` and `manifest.ts` go from 45 adapter-name branches to 0. The Mobile bridge, HUD, native-provider, lifecycle and runtime-identity code moves to `src/adapters/mobile/` and loads on first use, as does the Extension page helper. The test-only `example` adapter runs a recipe through the runner (manifest, `cdp.target`, `ui.*`, a bundled action, a live script) with no shared-file edit. Flags, output, and the CLI goldens are unchanged.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `pr-body render` counts the nodes of a Recipe v1 graph (`workflow.nodes`). The recipe summary read `recipe.json (0 steps …)` for every v1 recipe; it now reads `recipe.json (<n> nodes …)` with the authored nodes (a `call` node once), next to the run log's executed node count.
|
|
19
|
+
- Several `mm-harness` commands starting at once no longer race on dependency links. The local Farmslot relink (`FARMSLOT_ROOT`, `dev:link-farmslot`) holds one `proper-lockfile` lock, reclaimed when its holder dies, and swaps each link with `rename()`, so runs no longer fail with `EEXIST`/`ENOENT` or see a package missing. The self-package link is renamed into place too, so a fresh install no longer gets a stray link inside the checkout.
|
|
20
|
+
|
|
21
|
+
## 0.74.0 - 2026-10-03
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Adapters register in an `AdapterRegistry` (`src/adapters/registry.ts`) instead of a compiled-in union: the adapter id is a string, the four built-in surfaces register themselves, and `launch` resolves `--adapter` through the registry (the CLI front door still accepts only the four built-ins). `launch` moves behind a new `AdapterSurface.launch` member, so the command keeps only the shared grammar and no longer branches on the adapter name (39 such branches in `commands/launch/index.ts`, now 0). A test-only `example` adapter registers and launches with no shared-file edit. Flags, output, and the CLI goldens are unchanged.
|
|
26
|
+
|
|
5
27
|
## 0.73.0 - 2026-10-03
|
|
6
28
|
|
|
7
29
|
### Changed
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ mm-harness fixtures init --from /secure/path/wallet-fixture.json
|
|
|
84
84
|
mm-harness fixtures init --dev
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Use `mm-harness update` to update a published installation.
|
|
87
|
+
Use `mm-harness update` to update a published installation. `npm i -g` and `update` rewrite the global install in place, so a task that must survive an upgrade records `mm-harness pin` instead: it copies the running install into an immutable, versioned store and prints the copy's executable.
|
|
88
88
|
|
|
89
89
|
## Operate
|
|
90
90
|
|
|
@@ -30,10 +30,22 @@ deps_ready() {
|
|
|
30
30
|
# is not npm-installed into its own node_modules, so symlink self for resolution.
|
|
31
31
|
ensure_self_package_link() {
|
|
32
32
|
local link="$RUNNER_DIR/node_modules/@deeeed/metamask-harness"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ln -sf "$RUNNER_DIR" "$link"
|
|
33
|
+
if [ -e "$link" ]; then
|
|
34
|
+
return 0
|
|
36
35
|
fi
|
|
36
|
+
# Several bins can start at once on a fresh install. `ln -sf` would follow a link another
|
|
37
|
+
# run just made and create a second one inside the runner checkout, so stage the link and
|
|
38
|
+
# rename it into place: rename() replaces a link without following it.
|
|
39
|
+
node -e '
|
|
40
|
+
const fs = require("node:fs");
|
|
41
|
+
const path = require("node:path");
|
|
42
|
+
const [runnerDir, link] = process.argv.slice(1);
|
|
43
|
+
fs.mkdirSync(path.dirname(link), { recursive: true });
|
|
44
|
+
const staged = `${link}.${process.pid}`;
|
|
45
|
+
fs.rmSync(staged, { force: true });
|
|
46
|
+
fs.symlinkSync(runnerDir, staged, "dir");
|
|
47
|
+
fs.renameSync(staged, link);
|
|
48
|
+
' "$RUNNER_DIR" "$link"
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
ensure_dev_package_condition() {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
3
|
+
}
|
|
4
|
+
function traceText(value) {
|
|
5
|
+
if (value === void 0 || value === null) return "";
|
|
6
|
+
if (value instanceof Error) return value.message;
|
|
7
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
isRecord,
|
|
14
|
+
traceText
|
|
15
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { manifestPath } from "../../paths.js";
|
|
2
|
+
const coreActions = {
|
|
3
|
+
manifestPath: () => manifestPath("core"),
|
|
4
|
+
semantic: [
|
|
5
|
+
"metamask.perps.close_positions",
|
|
6
|
+
"metamask.perps.start_state",
|
|
7
|
+
"metamask.perps.read_snapshot",
|
|
8
|
+
"metamask.perps.read_account"
|
|
9
|
+
],
|
|
10
|
+
// read_account is Core's alone, and only its live adapter implements it.
|
|
11
|
+
liveOnly: ["metamask.perps.read_account"],
|
|
12
|
+
tsxLiveScripts: {
|
|
13
|
+
env: async (projectRoot, tempDir) => (await import("./live-script-env.js")).coreLiveScriptEnv(projectRoot, tempDir)
|
|
14
|
+
},
|
|
15
|
+
// Core declares no cdp.target; the probe matches the Extension's.
|
|
16
|
+
cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" }
|
|
17
|
+
};
|
|
18
|
+
export {
|
|
19
|
+
coreActions
|
|
20
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { corePnpNodeOptions } from "../../commands/core-readiness.js";
|
|
5
|
+
const CORE_WORKSPACE_PACKAGES = [
|
|
6
|
+
"@metamask/base-controller",
|
|
7
|
+
"@metamask/messenger",
|
|
8
|
+
"@metamask/controller-utils",
|
|
9
|
+
"@metamask/keyring-controller"
|
|
10
|
+
];
|
|
11
|
+
async function coreLiveScriptEnv(projectRoot, tempDir) {
|
|
12
|
+
const paths = {};
|
|
13
|
+
for (const pkg of CORE_WORKSPACE_PACKAGES) {
|
|
14
|
+
const packageDir = path.join(projectRoot, "packages", pkg.replace("@metamask/", ""));
|
|
15
|
+
const distEntry = path.join(packageDir, "dist/index.cjs");
|
|
16
|
+
if (existsSync(distEntry)) continue;
|
|
17
|
+
const srcDir = path.join(packageDir, "src");
|
|
18
|
+
if (!existsSync(path.join(srcDir, "index.ts"))) continue;
|
|
19
|
+
paths[pkg] = [path.join(srcDir, "index.ts")];
|
|
20
|
+
paths[`${pkg}/*`] = [path.join(srcDir, "*")];
|
|
21
|
+
}
|
|
22
|
+
if (Object.keys(paths).length === 0) return {};
|
|
23
|
+
const tsconfigPath = path.join(tempDir, "core-adapter.tsconfig.json");
|
|
24
|
+
await writeFile(
|
|
25
|
+
tsconfigPath,
|
|
26
|
+
`${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
|
|
27
|
+
`
|
|
28
|
+
);
|
|
29
|
+
return {
|
|
30
|
+
TSX_TSCONFIG_PATH: tsconfigPath,
|
|
31
|
+
NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
coreLiveScriptEnv
|
|
36
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { coreDependencyBlock } from "../../commands/core-readiness.js";
|
|
2
|
+
import { isRecord } from "../../commands/parse-args.js";
|
|
3
|
+
const coreRun = {
|
|
4
|
+
async dependencyBlock(target, { recipe, librarySources, action }) {
|
|
5
|
+
const usesController = action !== void 0 ? actionUsesCoreController(action) : await recipeUsesCoreController(recipe, librarySources);
|
|
6
|
+
return usesController ? coreDependencyBlock(target) : null;
|
|
7
|
+
},
|
|
8
|
+
violationUserAction: (violation) => violation.code === "WALLET_STATE_REQUIRED" ? "for signing, configure a reserved wallet-fixture.json account and pass its non-empty name; an explicit 0x address is supported only by read-only actions" : void 0
|
|
9
|
+
};
|
|
10
|
+
async function recipeUsesCoreController(recipe, librarySources) {
|
|
11
|
+
if (objectUsesCoreController(recipe)) return true;
|
|
12
|
+
const refs = collectCallRefs(recipe);
|
|
13
|
+
if (refs.length === 0) return false;
|
|
14
|
+
const sources = librarySources ?? [];
|
|
15
|
+
if (sources.length === 0) return false;
|
|
16
|
+
const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
|
|
17
|
+
const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
|
|
18
|
+
const visited = /* @__PURE__ */ new Set();
|
|
19
|
+
const visit = (ref) => {
|
|
20
|
+
if (visited.has(ref)) return false;
|
|
21
|
+
visited.add(ref);
|
|
22
|
+
const dependency = recipes.get(ref);
|
|
23
|
+
if (!dependency) return false;
|
|
24
|
+
if (objectUsesCoreController(dependency.document)) return true;
|
|
25
|
+
return collectCallRefs(dependency.document).some(visit);
|
|
26
|
+
};
|
|
27
|
+
return refs.some(visit);
|
|
28
|
+
}
|
|
29
|
+
function objectUsesCoreController(value) {
|
|
30
|
+
if (Array.isArray(value)) return value.some(objectUsesCoreController);
|
|
31
|
+
if (!isRecord(value)) return false;
|
|
32
|
+
if (actionUsesCoreController(value.action)) return true;
|
|
33
|
+
return Object.values(value).some(objectUsesCoreController);
|
|
34
|
+
}
|
|
35
|
+
function collectCallRefs(value) {
|
|
36
|
+
const refs = [];
|
|
37
|
+
const visit = (node) => {
|
|
38
|
+
if (Array.isArray(node)) {
|
|
39
|
+
for (const item of node) visit(item);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(node)) return;
|
|
43
|
+
if (node.action === "call" && typeof node.ref === "string" && node.ref) refs.push(node.ref);
|
|
44
|
+
for (const child of Object.values(node)) visit(child);
|
|
45
|
+
};
|
|
46
|
+
visit(value);
|
|
47
|
+
return [...new Set(refs)];
|
|
48
|
+
}
|
|
49
|
+
function actionUsesCoreController(action) {
|
|
50
|
+
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
coreRun
|
|
54
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
3
3
|
import { shellQuote } from "../../commands/parse-args.js";
|
|
4
|
+
import { adapterRegistry } from "../registry.js";
|
|
5
|
+
import { coreActions } from "./actions.js";
|
|
6
|
+
import { coreRun } from "./run.js";
|
|
4
7
|
const coreSurface = {
|
|
5
8
|
adapter: "core",
|
|
6
9
|
headless: true,
|
|
@@ -53,8 +56,12 @@ const coreSurface = {
|
|
|
53
56
|
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
54
57
|
relaunch: "mm-harness verify",
|
|
55
58
|
runtimeProbeRecovery: (target) => `mm-harness verify --adapter core --target ${shellQuote(path.resolve(target))}`
|
|
56
|
-
}
|
|
59
|
+
},
|
|
60
|
+
launch: async ({ usage }) => usage("core is headless; there is nothing to launch.", "mm-harness verify"),
|
|
61
|
+
actions: coreActions,
|
|
62
|
+
run: coreRun
|
|
57
63
|
};
|
|
64
|
+
adapterRegistry.register(coreSurface);
|
|
58
65
|
export {
|
|
59
66
|
coreSurface
|
|
60
67
|
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function extensionActionInputFindings(nodeId, node) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
const action = node.action;
|
|
7
|
+
const maximum = (name, limit) => {
|
|
8
|
+
if (typeof node[name] !== "number" || node[name] <= limit) return;
|
|
9
|
+
findings.push({
|
|
10
|
+
severity: "error",
|
|
11
|
+
code: "recipe.invalid_param",
|
|
12
|
+
path: `workflow.nodes.${nodeId}.${name}`,
|
|
13
|
+
message: `${action} ${name} must not exceed ${limit}`
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
if (action === "metamask.perps.read_snapshot") {
|
|
17
|
+
if (node.market !== void 0 && node.markets !== void 0) {
|
|
18
|
+
findings.push({
|
|
19
|
+
severity: "error",
|
|
20
|
+
code: "recipe.invalid_param",
|
|
21
|
+
path: `workflow.nodes.${nodeId}.markets`,
|
|
22
|
+
message: `${action} accepts market or markets, not both`
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (isRecord(node.history)) {
|
|
26
|
+
const start = node.history.start_time_ms;
|
|
27
|
+
const end = node.history.end_time_ms;
|
|
28
|
+
if (typeof start === "number" && typeof end === "number" && start > end) {
|
|
29
|
+
findings.push({
|
|
30
|
+
severity: "error",
|
|
31
|
+
code: "recipe.invalid_param",
|
|
32
|
+
path: `workflow.nodes.${nodeId}.history.start_time_ms`,
|
|
33
|
+
message: `${action} history.start_time_ms must not exceed history.end_time_ms`
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (typeof node.history.limit === "number" && node.history.limit > 500) {
|
|
37
|
+
findings.push({
|
|
38
|
+
severity: "error",
|
|
39
|
+
code: "recipe.invalid_param",
|
|
40
|
+
path: `workflow.nodes.${nodeId}.history.limit`,
|
|
41
|
+
message: `${action} history.limit must not exceed 500`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (typeof node.max_items_per_section === "number" && node.max_items_per_section > 500) {
|
|
46
|
+
findings.push({
|
|
47
|
+
severity: "error",
|
|
48
|
+
code: "recipe.invalid_param",
|
|
49
|
+
path: `workflow.nodes.${nodeId}.max_items_per_section`,
|
|
50
|
+
message: `${action} max_items_per_section must not exceed 500`
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (typeof node.timeout_ms === "number" && node.timeout_ms > 6e4) {
|
|
54
|
+
findings.push({
|
|
55
|
+
severity: "error",
|
|
56
|
+
code: "recipe.invalid_param",
|
|
57
|
+
path: `workflow.nodes.${nodeId}.timeout_ms`,
|
|
58
|
+
message: `${action} timeout_ms must not exceed 60000`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (isRecord(node.sampling)) {
|
|
62
|
+
if (typeof node.sampling.count === "number" && node.sampling.count > 10) {
|
|
63
|
+
findings.push({
|
|
64
|
+
severity: "error",
|
|
65
|
+
code: "recipe.invalid_param",
|
|
66
|
+
path: `workflow.nodes.${nodeId}.sampling.count`,
|
|
67
|
+
message: `${action} sampling.count must not exceed 10`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (typeof node.sampling.interval_ms === "number" && node.sampling.interval_ms > 3e4) {
|
|
71
|
+
findings.push({
|
|
72
|
+
severity: "error",
|
|
73
|
+
code: "recipe.invalid_param",
|
|
74
|
+
path: `workflow.nodes.${nodeId}.sampling.interval_ms`,
|
|
75
|
+
message: `${action} sampling.interval_ms must not exceed 30000`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (isRecord(node.expect)) {
|
|
80
|
+
if (typeof node.expect.minimum_changed_market_count === "number" && node.expect.minimum_changed_market_count > 100) {
|
|
81
|
+
findings.push({
|
|
82
|
+
severity: "error",
|
|
83
|
+
code: "recipe.invalid_param",
|
|
84
|
+
path: `workflow.nodes.${nodeId}.expect.minimum_changed_market_count`,
|
|
85
|
+
message: `${action} expect.minimum_changed_market_count must not exceed 100`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (typeof node.expect.minimum_history_count === "number" && node.expect.minimum_history_count > 500) {
|
|
89
|
+
findings.push({
|
|
90
|
+
severity: "error",
|
|
91
|
+
code: "recipe.invalid_param",
|
|
92
|
+
path: `workflow.nodes.${nodeId}.expect.minimum_history_count`,
|
|
93
|
+
message: `${action} expect.minimum_history_count must not exceed 500`
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (action === "metamask.perps.assert_visible_consistency") {
|
|
99
|
+
maximum("sample_count", 10);
|
|
100
|
+
maximum("sample_interval_ms", 3e4);
|
|
101
|
+
maximum("minimum_changed_count", 200);
|
|
102
|
+
maximum("minimum_recent_activity_count", 50);
|
|
103
|
+
maximum("timeout_ms", 12e4);
|
|
104
|
+
maximum("max_items", 200);
|
|
105
|
+
}
|
|
106
|
+
const fundedVisibleMutation = 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;
|
|
107
|
+
if (fundedVisibleMutation) {
|
|
108
|
+
const required = (name, pattern, message) => {
|
|
109
|
+
if (typeof node[name] === "string" && pattern.test(node[name])) return;
|
|
110
|
+
findings.push({
|
|
111
|
+
severity: "error",
|
|
112
|
+
code: "recipe.invalid_param",
|
|
113
|
+
path: `workflow.nodes.${nodeId}.${name}`,
|
|
114
|
+
message
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const decimal = action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? node.funding_amount : node.amount ?? node.notional;
|
|
118
|
+
const decimalText = String(decimal ?? "");
|
|
119
|
+
const match = /^([1-9][0-9]*)(?:\.([0-9]{1,6}))?$/u.exec(decimalText);
|
|
120
|
+
if (!match) {
|
|
121
|
+
findings.push({
|
|
122
|
+
severity: "error",
|
|
123
|
+
code: "recipe.invalid_param",
|
|
124
|
+
path: `workflow.nodes.${nodeId}.${action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? "funding_amount" : "amount"}`,
|
|
125
|
+
message: `${action} funded decimal amount must be positive plain decimal text with at most six places`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (action === "metamask.perps.place_order" && node.submit_attempts !== void 0 && node.submit_attempts !== 1) {
|
|
129
|
+
findings.push({
|
|
130
|
+
severity: "error",
|
|
131
|
+
code: "recipe.invalid_param",
|
|
132
|
+
path: `workflow.nodes.${nodeId}.submit_attempts`,
|
|
133
|
+
message: `${action} funded visible submission requires submit_attempts=1`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (action === "metamask.perps.close_visible_position" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
137
|
+
required("funding_side", /^(?:long|short)$/u, `${action} requires the authorized position side`);
|
|
138
|
+
}
|
|
139
|
+
if (action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
140
|
+
required(
|
|
141
|
+
"funding_position_size",
|
|
142
|
+
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
143
|
+
`${action} requires the exact visible position size`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (action === "metamask.perps.close_visible_position" && node.cleanup_only === true) {
|
|
147
|
+
required(
|
|
148
|
+
"funding_position_size",
|
|
149
|
+
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
150
|
+
`${action} cleanup requires the exact visible position size`
|
|
151
|
+
);
|
|
152
|
+
if (node.amount != null) {
|
|
153
|
+
findings.push({
|
|
154
|
+
severity: "error",
|
|
155
|
+
code: "recipe.invalid_param",
|
|
156
|
+
path: `workflow.nodes.${nodeId}.amount`,
|
|
157
|
+
message: `${action} cleanup must close the full exact position`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (action === "metamask.perps.edit_margin" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.funding_amount ?? ""))) {
|
|
162
|
+
findings.push({
|
|
163
|
+
severity: "error",
|
|
164
|
+
code: "recipe.invalid_param",
|
|
165
|
+
path: `workflow.nodes.${nodeId}.funding_amount`,
|
|
166
|
+
message: `${action} requires the authorized position amount`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (action === "metamask.perps.close_orders") {
|
|
170
|
+
required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
|
|
171
|
+
const outputPrice = /^\{\{outputs\.[A-Za-z0-9_.-]+\}\}$/u.test(String(node.limit_price ?? ""));
|
|
172
|
+
if (!outputPrice && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
173
|
+
findings.push({
|
|
174
|
+
severity: "error",
|
|
175
|
+
code: "recipe.invalid_param",
|
|
176
|
+
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
177
|
+
message: `${action} requires an explicit positive limit_price for resumable cancellation`
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
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))) {
|
|
182
|
+
findings.push({
|
|
183
|
+
severity: "error",
|
|
184
|
+
code: "recipe.invalid_param",
|
|
185
|
+
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
186
|
+
message: `${action} funded limit submission requires explicit limit_price; offset-only pricing is not resumable`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const name of ["identity_rounding_tolerance_base_units", "balance_reconciliation_tolerance_base_units"]) {
|
|
190
|
+
if (node[name] === void 0) continue;
|
|
191
|
+
const value = String(node[name]);
|
|
192
|
+
if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(value) || BigInt(value) > 1000000n) {
|
|
193
|
+
findings.push({
|
|
194
|
+
severity: "error",
|
|
195
|
+
code: "recipe.invalid_param",
|
|
196
|
+
path: `workflow.nodes.${nodeId}.${name}`,
|
|
197
|
+
message: `${action} ${name} must be an integer string from 0 through 1000000`
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return findings;
|
|
203
|
+
}
|
|
204
|
+
export {
|
|
205
|
+
extensionActionInputFindings
|
|
206
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { manifestPath } from "../../paths.js";
|
|
2
|
+
import { extensionActionInputFindings } from "./action-inputs.js";
|
|
3
|
+
const extensionActions = {
|
|
4
|
+
manifestPath: () => manifestPath("extension"),
|
|
5
|
+
semantic: [
|
|
6
|
+
"app.network_capture",
|
|
7
|
+
"app.network_assert",
|
|
8
|
+
"app.performance_capture",
|
|
9
|
+
"app.performance_assert",
|
|
10
|
+
"metamask.assets.prepare_send",
|
|
11
|
+
"metamask.assets.review_send",
|
|
12
|
+
"metamask.assets.set_custom_gas",
|
|
13
|
+
"metamask.assets.select_network_scope",
|
|
14
|
+
"metamask.networks.add_chainlist",
|
|
15
|
+
"metamask.perps.compare_provider_market",
|
|
16
|
+
"metamask.perps.read_eligibility",
|
|
17
|
+
"metamask.perps.open_position_action",
|
|
18
|
+
"metamask.perps.edit_margin",
|
|
19
|
+
"metamask.perps.update_position_tpsl",
|
|
20
|
+
"metamask.perps.assert_visible_consistency",
|
|
21
|
+
"metamask.settings.set_basic_functionality",
|
|
22
|
+
"metamask.perps.read_snapshot"
|
|
23
|
+
],
|
|
24
|
+
liveNeedsCdpPort: true,
|
|
25
|
+
cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" },
|
|
26
|
+
ui: (harness) => ({
|
|
27
|
+
base: harness.createCdpWebUiTransport({
|
|
28
|
+
async withPage(input, callback) {
|
|
29
|
+
const { withExtensionPage } = await import("../../../library/actions/extension/platform/cdp.mjs");
|
|
30
|
+
return withExtensionPage(
|
|
31
|
+
{
|
|
32
|
+
action: input.action,
|
|
33
|
+
node: input.node,
|
|
34
|
+
context: {
|
|
35
|
+
nodeId: input.context.nodeId,
|
|
36
|
+
projectRoot: input.context.projectRoot,
|
|
37
|
+
artifactsDir: input.context.artifactsDir
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
callback
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}),
|
|
45
|
+
trustedMutation: true,
|
|
46
|
+
inputFindings: extensionActionInputFindings,
|
|
47
|
+
ensureChecksOnly: true
|
|
48
|
+
};
|
|
49
|
+
export {
|
|
50
|
+
extensionActions
|
|
51
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
|
|
1
3
|
import { recipeRuntimePath } from "../../paths.js";
|
|
2
4
|
import { ensureConsoleCollector } from "../console-capture.js";
|
|
3
5
|
function extensionCaptureFiles(target) {
|
|
@@ -25,8 +27,18 @@ async function ensureExtensionConsoleCapture(target) {
|
|
|
25
27
|
cwd: target
|
|
26
28
|
});
|
|
27
29
|
}
|
|
30
|
+
async function verifyExtensionConsoleControl(capture) {
|
|
31
|
+
let extensionId = null;
|
|
32
|
+
try {
|
|
33
|
+
extensionId = fs.readFileSync(recipeRuntimePath(capture.projectRoot, "extension.id"), "utf8").trim();
|
|
34
|
+
} catch {
|
|
35
|
+
extensionId = null;
|
|
36
|
+
}
|
|
37
|
+
return verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
|
|
38
|
+
}
|
|
28
39
|
export {
|
|
29
40
|
ensureExtensionConsoleCapture,
|
|
30
41
|
extensionCaptureFiles,
|
|
31
|
-
extensionCdpPort
|
|
42
|
+
extensionCdpPort,
|
|
43
|
+
verifyExtensionConsoleControl
|
|
32
44
|
};
|