@deeeed/metamask-harness 0.17.4 → 0.18.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 +775 -0
- package/README.md +65 -71
- package/adapters/extension/ensure-browser.sh +12 -1
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/launch-browser.cjs +10 -3
- package/adapters/extension/lib/chrome-args.cjs +31 -0
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +10 -20
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/verify.sh +15 -3
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
- package/adapters/shared/resolve-slot-ports.sh +22 -5
- package/bin/mm-harness +15 -3
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +3 -1
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -71
- package/dist/adapters/extension/surface.js +19 -1
- package/dist/adapters/mobile/prepare.js +17 -0
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +4 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +13 -32
- package/dist/adapters.js +50 -17
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -3
- package/dist/command-contract.js +13 -3
- package/dist/commands/call.js +115 -29
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/debug.js +31 -38
- package/dist/commands/doctor.js +33 -6
- package/dist/commands/fixtures.js +65 -17
- package/dist/commands/flows.js +39 -10
- package/dist/commands/launch/extension.js +40 -15
- package/dist/commands/launch/index.js +41 -5
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/manifest.js +127 -18
- package/dist/commands/parse-args.js +11 -1
- package/dist/commands/run-engine.js +384 -56
- package/dist/commands/run.js +112 -17
- package/dist/commands/shared.js +22 -2
- package/dist/commands/status-probe.js +3 -0
- package/dist/commands/status.js +1 -0
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +56 -6
- package/dist/harness.js +6 -5
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +132 -12
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +13 -7
- package/dist/recipe-security.js +178 -0
- package/dist/runner.js +87 -13
- package/dist/runtime-context.js +8 -26
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +61 -0
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +53 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/package.json +10 -6
- package/scripts/completions.sh +7 -7
- package/docs/ADAPTER-SURFACE.md +0 -26
- package/docs/CHEATSHEET.md +0 -48
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/CODE-MAP.md +0 -62
- package/docs/UX-PRINCIPLES.md +0 -66
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/docs/architecture.md +0 -88
- package/docs/live-adapter-contract.md +0 -190
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -71
- package/docs/runtime-file-conventions.md +0 -36
package/dist/adapters.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
runLiveAdapterScript
|
|
5
|
+
} from "./live-adapter-contract.js";
|
|
4
6
|
import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
|
|
5
7
|
import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
|
|
6
8
|
function sleep(ms) {
|
|
@@ -32,6 +34,7 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
32
34
|
"metamask.wallet.setup",
|
|
33
35
|
"metamask.wallet.ensure_unlocked",
|
|
34
36
|
"metamask.wallet.select_account",
|
|
37
|
+
"metamask.wallet.list_accounts",
|
|
35
38
|
"metamask.wallet.read_state"
|
|
36
39
|
]);
|
|
37
40
|
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set(["ui.navigate"]);
|
|
@@ -42,14 +45,20 @@ function liveRuntimeConfigured(platform, node) {
|
|
|
42
45
|
if (platform === "mobile" || platform === "core") return true;
|
|
43
46
|
return Boolean(node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
|
|
44
47
|
}
|
|
45
|
-
async function runLiveFirst(platform, action, node, context) {
|
|
48
|
+
async function runLiveFirst(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
|
|
46
49
|
if (node.allow_static_placeholder === true) {
|
|
47
50
|
throw new Error(
|
|
48
51
|
`${action} refused allow_static_placeholder. MetaMask runner proof runs must use live adapters and real artifacts.`
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
|
-
if (!requiresLiveAdapter(platform, action) && !liveRuntimeConfigured(platform, node)) return null;
|
|
52
|
-
const live = await runLiveAdapterScript({
|
|
54
|
+
if (!forceLive && !requiresLiveAdapter(platform, action) && !liveRuntimeConfigured(platform, node)) return null;
|
|
55
|
+
const live = await runLiveAdapterScript({
|
|
56
|
+
platform,
|
|
57
|
+
action,
|
|
58
|
+
node,
|
|
59
|
+
context,
|
|
60
|
+
prepared: preparedLiveAdapters?.get(action)
|
|
61
|
+
});
|
|
53
62
|
if (!live) return null;
|
|
54
63
|
const liveResult = isRecord(live.result) ? live.result : { result: live.result };
|
|
55
64
|
const output = { ...liveResult, liveAdapter: live.script };
|
|
@@ -64,10 +73,17 @@ function liveAdapterPathHint(platform, action) {
|
|
|
64
73
|
}
|
|
65
74
|
return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
|
|
66
75
|
}
|
|
67
|
-
async function semanticResult(platform, action, node, context) {
|
|
68
|
-
const live = await runLiveFirst(
|
|
76
|
+
async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
|
|
77
|
+
const live = await runLiveFirst(
|
|
78
|
+
platform,
|
|
79
|
+
action,
|
|
80
|
+
node,
|
|
81
|
+
context,
|
|
82
|
+
forceLive,
|
|
83
|
+
preparedLiveAdapters
|
|
84
|
+
);
|
|
69
85
|
if (live) return live;
|
|
70
|
-
if (requiresLiveAdapter(platform, action)) {
|
|
86
|
+
if (forceLive || requiresLiveAdapter(platform, action)) {
|
|
71
87
|
const expected = liveAdapterPathHint(platform, action);
|
|
72
88
|
throw new Error(
|
|
73
89
|
`${action} requires a live ${platform} adapter that drives a real supported app/API path; no adapter script was found or no live runtime is configured (for example ${expected}). Static placeholders are refused to avoid fabricated proof. Set METAMASK_RECIPE_LIVE_ADAPTER_DIR or add a runner library/actions script.`
|
|
@@ -145,15 +161,16 @@ function probeHttpJson(url, timeoutMs = 1e3) {
|
|
|
145
161
|
});
|
|
146
162
|
});
|
|
147
163
|
}
|
|
148
|
-
function createMetaMaskSemanticAdapters(platform) {
|
|
164
|
+
function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], preparedLiveAdapters) {
|
|
149
165
|
const walletActions = [
|
|
150
166
|
"metamask.wallet.fixture_status",
|
|
151
167
|
"metamask.wallet.setup",
|
|
152
168
|
"metamask.wallet.ensure_unlocked",
|
|
153
169
|
"metamask.wallet.select_account",
|
|
170
|
+
"metamask.wallet.list_accounts",
|
|
154
171
|
"metamask.wallet.read_state"
|
|
155
172
|
];
|
|
156
|
-
const
|
|
173
|
+
const bundledActions = [
|
|
157
174
|
...walletActions,
|
|
158
175
|
"metamask.perps.read_positions",
|
|
159
176
|
"metamask.perps.ensure_positions",
|
|
@@ -169,10 +186,19 @@ function createMetaMaskSemanticAdapters(platform) {
|
|
|
169
186
|
// read_account is core-only: only the headless core adapter implements it.
|
|
170
187
|
...platform === "core" ? ["metamask.perps.read_account"] : []
|
|
171
188
|
];
|
|
189
|
+
const bundled = new Set(bundledActions);
|
|
190
|
+
const actions = [.../* @__PURE__ */ new Set([...bundledActions, ...declaredCustomActions])];
|
|
172
191
|
return actions.map(
|
|
173
192
|
(action) => simpleAdapter(
|
|
174
193
|
action,
|
|
175
|
-
async (node, context) => semanticResult(
|
|
194
|
+
async (node, context) => semanticResult(
|
|
195
|
+
platform,
|
|
196
|
+
action,
|
|
197
|
+
node,
|
|
198
|
+
context,
|
|
199
|
+
!bundled.has(action),
|
|
200
|
+
preparedLiveAdapters
|
|
201
|
+
)
|
|
176
202
|
)
|
|
177
203
|
);
|
|
178
204
|
}
|
|
@@ -439,7 +465,7 @@ function mobileHudStatusLabel(status) {
|
|
|
439
465
|
if (status === "pass") return "pass";
|
|
440
466
|
return "run";
|
|
441
467
|
}
|
|
442
|
-
function createMetaMaskUiTransport(platform, harness) {
|
|
468
|
+
function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
443
469
|
if (platform === "core") {
|
|
444
470
|
return {
|
|
445
471
|
async execute(action) {
|
|
@@ -468,19 +494,26 @@ function createMetaMaskUiTransport(platform, harness) {
|
|
|
468
494
|
return {
|
|
469
495
|
async execute(action, node, context) {
|
|
470
496
|
if (action === "ui.navigate") {
|
|
471
|
-
const live = await runLiveFirst(
|
|
497
|
+
const live = await runLiveFirst(
|
|
498
|
+
platform,
|
|
499
|
+
action,
|
|
500
|
+
node,
|
|
501
|
+
context,
|
|
502
|
+
false,
|
|
503
|
+
preparedLiveAdapters
|
|
504
|
+
);
|
|
472
505
|
if (live) return live.output;
|
|
473
506
|
throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
|
|
474
507
|
}
|
|
475
|
-
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node
|
|
508
|
+
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
|
|
476
509
|
}
|
|
477
510
|
};
|
|
478
511
|
}
|
|
479
|
-
function normalizeUiWaitNode(node
|
|
480
|
-
if (
|
|
512
|
+
function normalizeUiWaitNode(node) {
|
|
513
|
+
if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
|
|
481
514
|
return { ...node, expected: node.visible ? "visible" : "absent" };
|
|
482
515
|
}
|
|
483
|
-
function createMetaMaskAdapters(adapter) {
|
|
516
|
+
function createMetaMaskAdapters(adapter, declaredCustomActions = [], preparedLiveAdapters) {
|
|
484
517
|
const platform = adapter;
|
|
485
518
|
const adapters = [
|
|
486
519
|
simpleAdapter("app.status", async (_node, context) => ({
|
|
@@ -532,7 +565,7 @@ function createMetaMaskAdapters(adapter) {
|
|
|
532
565
|
}
|
|
533
566
|
};
|
|
534
567
|
}),
|
|
535
|
-
...createMetaMaskSemanticAdapters(platform)
|
|
568
|
+
...createMetaMaskSemanticAdapters(platform, declaredCustomActions, preparedLiveAdapters)
|
|
536
569
|
];
|
|
537
570
|
return adapters;
|
|
538
571
|
}
|
package/dist/checkout-lock.js
CHANGED
|
@@ -7,7 +7,7 @@ function acquireCheckoutLock(target, operation) {
|
|
|
7
7
|
const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
|
|
8
8
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
9
9
|
const inheritedToken = process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
10
|
-
for (let attempt = 0; attempt <
|
|
10
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
11
11
|
try {
|
|
12
12
|
const fd = fs.openSync(lockPath, "wx", 384);
|
|
13
13
|
const token = inheritedToken || randomUUID();
|
|
@@ -46,11 +46,36 @@ function acquireCheckoutLock(target, operation) {
|
|
|
46
46
|
message: `checkout sandbox is busy with ${String(owner.operation ?? "another operation")} (pid ${owner.pid}).`
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
claimStaleLock(lockPath, owner);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
return { path: lockPath, owner: readOwner(lockPath), message: "checkout sandbox lock could not be acquired." };
|
|
53
53
|
}
|
|
54
|
+
function claimStaleLock(lockPath, expectedOwner) {
|
|
55
|
+
const claimedPath = `${lockPath}.stale-${process.pid}-${randomUUID()}`;
|
|
56
|
+
try {
|
|
57
|
+
fs.renameSync(lockPath, claimedPath);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code === "ENOENT") return;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const claimedOwner = readOwner(claimedPath);
|
|
64
|
+
if (!sameOwner(claimedOwner, expectedOwner) || claimedOwner && typeof claimedOwner.pid === "number" && processAlive(claimedOwner.pid)) {
|
|
65
|
+
try {
|
|
66
|
+
fs.linkSync(claimedPath, lockPath);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code !== "EEXIST") throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
fs.rmSync(claimedPath, { force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function sameOwner(left, right) {
|
|
76
|
+
if (!left || !right) return left === right;
|
|
77
|
+
return left.pid === right.pid && left.token === right.token && left.startedAt === right.startedAt && left.repoRoot === right.repoRoot;
|
|
78
|
+
}
|
|
54
79
|
function readOwner(lockPath) {
|
|
55
80
|
try {
|
|
56
81
|
const value = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
package/dist/cli-color.js
CHANGED
|
@@ -47,6 +47,24 @@ function colorStatusWord(word, { stream = process.stderr } = {}) {
|
|
|
47
47
|
}
|
|
48
48
|
return color("info", word, { stream });
|
|
49
49
|
}
|
|
50
|
+
function colorHumanMessage(message, { stream = process.stderr } = {}) {
|
|
51
|
+
return String(message).split("\n").map((line) => {
|
|
52
|
+
const marker = /^(\s*)(→|✓|✗)\s?(.*)$/u.exec(line);
|
|
53
|
+
if (marker) {
|
|
54
|
+
const style = marker[2] === "\u2713" ? "ok" : marker[2] === "\u2717" ? "err" : "accent";
|
|
55
|
+
return `${marker[1]}${color(style, marker[2], { stream })}${marker[3] ? ` ${marker[3]}` : ""}`;
|
|
56
|
+
}
|
|
57
|
+
const next = /^(\s*)Next:\s*(.*)$/u.exec(line);
|
|
58
|
+
if (next) {
|
|
59
|
+
return `${next[1]}${color("label", "Next:", { stream })}${next[2] ? ` ${color("cmd", next[2], { stream })}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
const warning = /^(\s*)(WARN(?:ING)?)(:?)[ \t]*(.*)$/iu.exec(line);
|
|
62
|
+
if (warning) {
|
|
63
|
+
return `${warning[1]}${color("warn", `${warning[2]}${warning[3]}`, { stream })}${warning[4] ? ` ${warning[4]}` : ""}`;
|
|
64
|
+
}
|
|
65
|
+
return line;
|
|
66
|
+
}).join("\n");
|
|
67
|
+
}
|
|
50
68
|
function classifyLogEvent(event) {
|
|
51
69
|
const text = String(event || "");
|
|
52
70
|
if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
|
|
@@ -72,6 +90,7 @@ export {
|
|
|
72
90
|
classifyLogEvent,
|
|
73
91
|
color,
|
|
74
92
|
colorEnabled,
|
|
93
|
+
colorHumanMessage,
|
|
75
94
|
colorKv,
|
|
76
95
|
colorLogEvent,
|
|
77
96
|
colorStatusWord,
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,7 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
16
16
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
17
17
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
18
|
-
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action"] },
|
|
18
|
+
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action", "--library"] },
|
|
19
19
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
20
20
|
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
21
21
|
{ name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,6 @@ import { handleCheck } from "./commands/check.js";
|
|
|
23
23
|
import { handleChecklist } from "./commands/checklist.js";
|
|
24
24
|
import { handleLast } from "./commands/last.js";
|
|
25
25
|
import { parseArgs, targetPath } from "./commands/parse-args.js";
|
|
26
|
-
import { runOneNode } from "./commands/run-engine.js";
|
|
27
26
|
const COMMANDS = {
|
|
28
27
|
actions: handleActions,
|
|
29
28
|
doctor: handleDoctor,
|
|
@@ -81,7 +80,7 @@ ONE bin: mm-harness is the only command. No per-platform binaries \u2014 platfor
|
|
|
81
80
|
auto-detected, the positional target forces it (mm-harness launch ios), and
|
|
82
81
|
platform-specific needs are FLAGS on the same command (e.g. --sidebar, --full-build).
|
|
83
82
|
Human happy path = the bare command; agents add depth via flags (--json, --target, ports).
|
|
84
|
-
See README.md for the
|
|
83
|
+
See README.md for the workflow and docs/RECIPES.md for proof authoring.
|
|
85
84
|
`);
|
|
86
85
|
}
|
|
87
86
|
async function main(argv) {
|
|
@@ -122,7 +121,7 @@ async function main(argv) {
|
|
|
122
121
|
if (command === "stop") return handleStop(argv.slice(1));
|
|
123
122
|
if (command === "logs") return handleLogs(argv.slice(1));
|
|
124
123
|
if (command === "debug") return handleDebug(argv.slice(1));
|
|
125
|
-
if (command === "fixtures") return handleFixtures(argv.slice(1)
|
|
124
|
+
if (command === "fixtures") return handleFixtures(argv.slice(1));
|
|
126
125
|
if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
|
|
127
126
|
if (command === "check") return handleCheck(argv.slice(1));
|
|
128
127
|
if (command === "checklist") return handleChecklist(argv.slice(1));
|
package/dist/command-contract.js
CHANGED
|
@@ -25,7 +25,12 @@ const RECIPE_RUNTIME = {
|
|
|
25
25
|
"--validation-runtime-dir": value(),
|
|
26
26
|
"--launch-existing-dist": bool(),
|
|
27
27
|
"--record-video": optionalValue(["full-run", "off"]),
|
|
28
|
-
"--record": bool()
|
|
28
|
+
"--record": bool(),
|
|
29
|
+
"--source-trust": value(["trusted", "untrusted", "unknown"]),
|
|
30
|
+
"--source-kind": value(["bundled", "operator", "task", "recipe-file", "uses-catalog", "library", "custom-adapter"]),
|
|
31
|
+
"--source-name": value(),
|
|
32
|
+
"--source-digest": value(),
|
|
33
|
+
"--approve-plan": value()
|
|
29
34
|
};
|
|
30
35
|
const RUNWAY = {
|
|
31
36
|
"--branch": value(),
|
|
@@ -82,6 +87,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
82
87
|
"--categories": bool(),
|
|
83
88
|
"--category": value(),
|
|
84
89
|
"--action-manifest": value(),
|
|
90
|
+
"--library": value(),
|
|
85
91
|
"--raw": bool()
|
|
86
92
|
}),
|
|
87
93
|
positionals: [{ label: "query" }],
|
|
@@ -115,12 +121,16 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
115
121
|
"--run": value(),
|
|
116
122
|
"--force": bool()
|
|
117
123
|
}),
|
|
118
|
-
positionals: [
|
|
124
|
+
positionals: [
|
|
125
|
+
{ label: "action", choices: ["list", "describe", "promote"] },
|
|
126
|
+
{ label: "ref" }
|
|
127
|
+
]
|
|
119
128
|
},
|
|
120
129
|
run: {
|
|
121
130
|
options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
|
|
122
131
|
"--list": bool(),
|
|
123
|
-
"--plan": bool()
|
|
132
|
+
"--plan": bool(),
|
|
133
|
+
"--describe": bool()
|
|
124
134
|
}),
|
|
125
135
|
positionals: [{ label: "recipe" }],
|
|
126
136
|
minimumPositionals: 1,
|
package/dist/commands/call.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
6
5
|
import { importRecipeProtocol } from "../paths.js";
|
|
7
6
|
import { recipeRunning } from "../heal-bounds.js";
|
|
8
7
|
import { color } from "../cli-color.js";
|
|
@@ -28,12 +27,14 @@ import {
|
|
|
28
27
|
prepareHeal,
|
|
29
28
|
recoverRunInfra,
|
|
30
29
|
resolveMetaMaskLibrarySources,
|
|
30
|
+
preflightRecipe,
|
|
31
31
|
runRecipe,
|
|
32
32
|
synthesizeOneNodeRecipe,
|
|
33
33
|
validateRecipeAdapterAware
|
|
34
34
|
} from "./run-engine.js";
|
|
35
35
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
36
36
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
37
|
+
import { recipeTrustFailure } from "../recipe-security.js";
|
|
37
38
|
async function handleCall(argv) {
|
|
38
39
|
if (argv.includes("--list")) {
|
|
39
40
|
const { options: options2 } = parseArgs(argv, "call");
|
|
@@ -52,7 +53,8 @@ async function handleCall(argv) {
|
|
|
52
53
|
let discovery = "mm-harness actions";
|
|
53
54
|
try {
|
|
54
55
|
const { adapter: adapter2 } = resolveAdapter(options);
|
|
55
|
-
const
|
|
56
|
+
const librarySources2 = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
57
|
+
const { manifest: manifest2 } = await resolveActionManifest(adapter2, optionString(options, "actionManifest"), librarySources2);
|
|
56
58
|
const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
|
|
57
59
|
const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
|
|
58
60
|
example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
|
|
@@ -101,7 +103,8 @@ async function handleCall(argv) {
|
|
|
101
103
|
return EXIT.bounded;
|
|
102
104
|
}
|
|
103
105
|
const actionManifestOverride = optionString(options, "actionManifest");
|
|
104
|
-
const
|
|
106
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
107
|
+
const { manifest } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
|
|
105
108
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
106
109
|
const names = getRecipeActionManifestActionNames(manifest);
|
|
107
110
|
const resolution = resolveActionName(shortName, names);
|
|
@@ -137,7 +140,7 @@ async function handleCall(argv) {
|
|
|
137
140
|
if (validation.status === "invalid") {
|
|
138
141
|
const message = `call ${resolvedAction}: recipe validation failed`;
|
|
139
142
|
const userAction = `mm-harness actions --action ${resolvedAction} --adapter ${adapter} --json`;
|
|
140
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
|
|
143
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
|
|
141
144
|
else {
|
|
142
145
|
console.error(`\u2717 ${message}`);
|
|
143
146
|
for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
@@ -145,6 +148,61 @@ async function handleCall(argv) {
|
|
|
145
148
|
}
|
|
146
149
|
return EXIT.validation;
|
|
147
150
|
}
|
|
151
|
+
const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction, args);
|
|
152
|
+
const requestedRuntimeOptions = runtimeOptionsFromCli(options);
|
|
153
|
+
const callRuntimeOptions = {
|
|
154
|
+
...requestedRuntimeOptions,
|
|
155
|
+
...librarySources ? { librarySources } : {},
|
|
156
|
+
autoHud: false,
|
|
157
|
+
stdoutIsMachineContract: json,
|
|
158
|
+
source: requestedRuntimeOptions.source ?? {
|
|
159
|
+
kind: "operator",
|
|
160
|
+
trust: "trusted",
|
|
161
|
+
name: "mm-harness call"
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let preflightedExecution;
|
|
165
|
+
try {
|
|
166
|
+
preflightedExecution = await preflightRecipe(
|
|
167
|
+
adapter,
|
|
168
|
+
recipe,
|
|
169
|
+
artifactsDir,
|
|
170
|
+
target,
|
|
171
|
+
actionManifestOverride,
|
|
172
|
+
callRuntimeOptions
|
|
173
|
+
);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const trustFailure = recipeTrustFailure(error);
|
|
176
|
+
if (!trustFailure) throw error;
|
|
177
|
+
const planDigest = trustFailure.details?.recipeDigest;
|
|
178
|
+
if (planDigest) {
|
|
179
|
+
trustFailure.userAction = `review the plan, then rerun this call with --artifacts-dir ${shellQuote(artifactsDir)} --approve-plan ${shellQuote(planDigest)}; managed callers must keep the same artifact directory and execution environment, then set FARMSLOT_RECIPE_APPROVE_PLAN=${shellQuote(planDigest)}`;
|
|
180
|
+
}
|
|
181
|
+
if (json) {
|
|
182
|
+
console.log(JSON.stringify({
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
command: "call",
|
|
185
|
+
status: "fail",
|
|
186
|
+
error: trustFailure,
|
|
187
|
+
exitCode: EXIT.validation
|
|
188
|
+
}, null, 2));
|
|
189
|
+
} else {
|
|
190
|
+
console.error(`\u2717 mm-harness call: ${trustFailure.message}`);
|
|
191
|
+
const blocked = trustFailure.details?.blocked;
|
|
192
|
+
if (blocked?.length) {
|
|
193
|
+
console.error(" Restricted plan nodes:");
|
|
194
|
+
for (const node of blocked.slice(0, 10)) {
|
|
195
|
+
const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
|
|
196
|
+
console.error(
|
|
197
|
+
` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
|
|
201
|
+
}
|
|
202
|
+
console.error(` Next: ${trustFailure.userAction}`);
|
|
203
|
+
}
|
|
204
|
+
return EXIT.validation;
|
|
205
|
+
}
|
|
148
206
|
const lock = acquireCheckoutLock(target, "call");
|
|
149
207
|
if ("message" in lock) {
|
|
150
208
|
return checkoutBusyOut(json, "call", lock.message, lock.path);
|
|
@@ -155,19 +213,20 @@ async function handleCall(argv) {
|
|
|
155
213
|
});
|
|
156
214
|
if (typeof prepared === "number") return prepared;
|
|
157
215
|
const { state, heal } = prepared;
|
|
158
|
-
const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
|
|
159
|
-
const recipeFile = path.join(scratch, "call.recipe.json");
|
|
160
|
-
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
161
|
-
`);
|
|
162
|
-
const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
|
|
163
|
-
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
164
|
-
const callRuntimeOptions = {
|
|
165
|
-
...runtimeOptionsFromCli(options),
|
|
166
|
-
...librarySources ? { librarySources } : {},
|
|
167
|
-
stdoutIsMachineContract: json
|
|
168
|
-
};
|
|
169
216
|
const { result, violation } = await executeWithHealBounds(
|
|
170
|
-
() =>
|
|
217
|
+
() => {
|
|
218
|
+
const execution = preflightedExecution;
|
|
219
|
+
preflightedExecution = void 0;
|
|
220
|
+
return runRecipe(
|
|
221
|
+
adapter,
|
|
222
|
+
recipe,
|
|
223
|
+
artifactsDir,
|
|
224
|
+
target,
|
|
225
|
+
actionManifestOverride,
|
|
226
|
+
callRuntimeOptions,
|
|
227
|
+
execution
|
|
228
|
+
);
|
|
229
|
+
},
|
|
171
230
|
adapter,
|
|
172
231
|
target,
|
|
173
232
|
heal,
|
|
@@ -176,6 +235,8 @@ async function handleCall(argv) {
|
|
|
176
235
|
);
|
|
177
236
|
if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
|
|
178
237
|
const callOutput = readCallOutput(result.tracePath);
|
|
238
|
+
const safeArgs = redactCallValue(args);
|
|
239
|
+
const safeCallOutput = redactCallValue(callOutput);
|
|
179
240
|
const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
|
|
180
241
|
if (json) {
|
|
181
242
|
console.log(
|
|
@@ -186,14 +247,14 @@ async function handleCall(argv) {
|
|
|
186
247
|
adapter,
|
|
187
248
|
action: shortName,
|
|
188
249
|
resolvedAction,
|
|
189
|
-
args,
|
|
250
|
+
args: safeArgs,
|
|
190
251
|
status: result.status,
|
|
191
252
|
summaryPath: result.summaryPath,
|
|
192
253
|
tracePath: result.tracePath,
|
|
193
254
|
artifactManifestPath: result.artifactManifestPath,
|
|
194
255
|
...result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {},
|
|
195
256
|
...result.sideFindings ? { sideFindings: result.sideFindings } : {},
|
|
196
|
-
...callOutput !== void 0 ? { output:
|
|
257
|
+
...callOutput !== void 0 ? { output: safeCallOutput } : {},
|
|
197
258
|
recovered: state.recovered,
|
|
198
259
|
mutations: state.mutations,
|
|
199
260
|
exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
@@ -204,9 +265,7 @@ async function handleCall(argv) {
|
|
|
204
265
|
)
|
|
205
266
|
);
|
|
206
267
|
} else {
|
|
207
|
-
const
|
|
208
|
-
Result:
|
|
209
|
-
${formatCallOutput(callOutput)}` : "";
|
|
268
|
+
const renderedInputs = Object.keys(args).length > 0 ? formatCallOutput(safeArgs) : "";
|
|
210
269
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
211
270
|
const diagnostics = formatRunDiagnosticsForHuman(
|
|
212
271
|
readRunDiagnosticsDocument(result.diagnosticsPath),
|
|
@@ -216,9 +275,11 @@ ${formatCallOutput(callOutput)}` : "";
|
|
|
216
275
|
${diagnostics.map((line) => ` ${line}`).join("\n")}
|
|
217
276
|
`;
|
|
218
277
|
console.log(
|
|
219
|
-
`${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${
|
|
278
|
+
`${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${renderedInputs ? `
|
|
279
|
+
${out("label", "Inputs:")}
|
|
280
|
+
${renderedInputs}` : ""}${callOutput !== void 0 ? `
|
|
220
281
|
${out("label", "Result:")}
|
|
221
|
-
${formatCallOutput(
|
|
282
|
+
${formatCallOutput(safeCallOutput)}` : ""}
|
|
222
283
|
` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
|
|
223
284
|
Next: ${failureUserAction}` : "")
|
|
224
285
|
);
|
|
@@ -228,6 +289,11 @@ ${formatCallOutput(callOutput)}` : ""}
|
|
|
228
289
|
lock.release();
|
|
229
290
|
}
|
|
230
291
|
}
|
|
292
|
+
function defaultCallArtifactsDir(target, action, args) {
|
|
293
|
+
const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
|
|
294
|
+
const digest = createHash("sha256").update(JSON.stringify({ action, args })).digest("hex").slice(0, 12);
|
|
295
|
+
return path.join(target, "temp", "recipe", "calls", `${actionStem}-${digest}`);
|
|
296
|
+
}
|
|
231
297
|
function readCallOutput(tracePath) {
|
|
232
298
|
try {
|
|
233
299
|
const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
|
|
@@ -243,20 +309,32 @@ function formatCallOutput(value) {
|
|
|
243
309
|
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
244
310
|
return JSON.stringify(value, null, 2);
|
|
245
311
|
}
|
|
312
|
+
function redactCallValue(value, key = "") {
|
|
313
|
+
if (/(?:password|secret|mnemonic|seed|srp|vault|private[_-]?key)/iu.test(key)) return "<redacted>";
|
|
314
|
+
if (Array.isArray(value)) return value.map((entry) => redactCallValue(entry));
|
|
315
|
+
if (isRecord(value)) {
|
|
316
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactCallValue(entry, entryKey)]));
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
246
320
|
async function handleCallHelp(argv, genericHelp) {
|
|
247
321
|
const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
|
|
248
322
|
const { options } = parseArgs(rest, "call");
|
|
249
323
|
let adapter;
|
|
250
324
|
let manifest;
|
|
325
|
+
let actionSources = /* @__PURE__ */ new Map();
|
|
251
326
|
try {
|
|
252
327
|
({ adapter } = resolveAdapter(options));
|
|
253
|
-
|
|
328
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
329
|
+
const resolution = await resolveActionManifest(adapter, optionString(options, "actionManifest"), librarySources);
|
|
330
|
+
manifest = resolution.manifest;
|
|
331
|
+
actionSources = resolution.actionSources;
|
|
254
332
|
} catch {
|
|
255
333
|
process.stdout.write(`${genericHelp}
|
|
256
334
|
`);
|
|
257
335
|
return EXIT.ok;
|
|
258
336
|
}
|
|
259
|
-
const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest), shortName) : [];
|
|
337
|
+
const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest, actionSources), shortName) : [];
|
|
260
338
|
if (matches.length === 0) {
|
|
261
339
|
process.stdout.write(`${genericHelp}
|
|
262
340
|
`);
|
|
@@ -284,7 +362,8 @@ function renderCallActionHelp(entry) {
|
|
|
284
362
|
);
|
|
285
363
|
const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
|
|
286
364
|
if (entry.description) lines.push(` ${entry.description}`, "");
|
|
287
|
-
|
|
365
|
+
lines.push(` Source: ${entry.source}${entry.sourceManifest ? ` (${entry.sourceManifest})` : ""}`, "");
|
|
366
|
+
const names = Object.keys(properties).filter((name) => name !== "action" && name !== "next").sort();
|
|
288
367
|
if (names.length === 0) {
|
|
289
368
|
lines.push(" Fields: (none)");
|
|
290
369
|
} else {
|
|
@@ -374,6 +453,7 @@ function isForwardedOptionValue(argv, index) {
|
|
|
374
453
|
}
|
|
375
454
|
const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
|
|
376
455
|
"action-manifest",
|
|
456
|
+
"approve-plan",
|
|
377
457
|
"adapter",
|
|
378
458
|
"artifacts-dir",
|
|
379
459
|
"cdp-port",
|
|
@@ -384,6 +464,10 @@ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
384
464
|
"platform",
|
|
385
465
|
"project-root",
|
|
386
466
|
"slot",
|
|
467
|
+
"source-digest",
|
|
468
|
+
"source-kind",
|
|
469
|
+
"source-name",
|
|
470
|
+
"source-trust",
|
|
387
471
|
"target",
|
|
388
472
|
"validation-runtime-dir",
|
|
389
473
|
"watcher-port"
|
|
@@ -407,6 +491,8 @@ function pickCallExampleAction(names) {
|
|
|
407
491
|
return names[0] ?? "command";
|
|
408
492
|
}
|
|
409
493
|
export {
|
|
494
|
+
defaultCallArtifactsDir,
|
|
410
495
|
handleCall,
|
|
411
|
-
handleCallHelp
|
|
496
|
+
handleCallHelp,
|
|
497
|
+
redactCallValue
|
|
412
498
|
};
|
|
@@ -4,6 +4,9 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { EXIT } from "./shared.js";
|
|
6
6
|
const require2 = createRequire(import.meta.url);
|
|
7
|
+
const MARK_SCRIPT = require2.resolve(
|
|
8
|
+
"@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
|
|
9
|
+
);
|
|
7
10
|
const USAGE = "mm-harness checklist mark <task-dir> <step> [options]";
|
|
8
11
|
function terminalSignalStatus(taskDir) {
|
|
9
12
|
try {
|
|
@@ -122,7 +125,7 @@ async function handleChecklist(argv) {
|
|
|
122
125
|
teachRecipeCompletion(resolvedTaskDir, markArgs);
|
|
123
126
|
const result = spawnSync(
|
|
124
127
|
process.execPath,
|
|
125
|
-
[
|
|
128
|
+
[MARK_SCRIPT, resolvedTaskDir, ...markArgs],
|
|
126
129
|
{ stdio: "inherit" }
|
|
127
130
|
);
|
|
128
131
|
if (result.error) {
|