@deeeed/metamask-harness 0.18.0 → 0.19.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 +187 -0
- package/README.md +4 -7
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/inject.mjs +1 -4
- package/adapters/extension/verify.sh +1 -1
- package/adapters/mobile/inject.sh +5 -4
- package/adapters/mobile/verify.sh +15 -19
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/cli.js +0 -4
- package/dist/command-contract.js +51 -40
- package/dist/command-journal.js +50 -12
- package/dist/commands/call.js +95 -20
- package/dist/commands/check.js +1 -1
- package/dist/commands/completion-candidates.js +7 -19
- package/dist/commands/fixtures.js +50 -1
- package/dist/commands/last.js +9 -1
- package/dist/commands/launch/extension.js +2 -2
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +56 -20
- package/dist/commands/manifest.js +28 -12
- package/dist/commands/parse-args.js +55 -3
- package/dist/commands/provision.js +0 -1
- package/dist/commands/run-engine.js +514 -336
- package/dist/commands/run.js +25 -37
- package/dist/commands/shared.js +1 -1
- package/dist/heal-bounds.js +5 -0
- package/dist/mm-harness-cli.js +22 -30
- package/dist/run-diagnostics.js +1 -1
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +24 -2
- package/docs/CONTRIBUTING.md +2 -3
- package/docs/QA.md +0 -1
- package/docs/RECIPES.md +52 -91
- package/library/README.md +5 -5
- package/library/actions/core/perps/_controller.mjs +33 -1
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1170 -413
- package/library/manifests/extension.action-manifest.json +1495 -641
- package/library/manifests/mobile.action-manifest.json +1704 -769
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
- package/library/recipes/runner/smoke.core.recipe.json +18 -20
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
- package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
- package/package.json +3 -3
- package/scripts/completions.sh +1 -4
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -91
- package/library/flows/perps.flows.json +0 -64
package/dist/commands/run.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveAdapter,
|
|
11
11
|
runtimeOptionsFromCli,
|
|
12
12
|
isRecord,
|
|
13
|
+
parseRecipeParamAssignments,
|
|
13
14
|
shellQuote,
|
|
14
15
|
targetPath,
|
|
15
16
|
usageError
|
|
@@ -33,6 +34,7 @@ import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
|
33
34
|
import { JsonStreamWriter } from "../json-stream.js";
|
|
34
35
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
35
36
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
37
|
+
import { recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
|
|
36
38
|
async function handleRun(parsed) {
|
|
37
39
|
const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
|
|
38
40
|
const restoreStdout = stream.isolateStdout();
|
|
@@ -104,7 +106,11 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
104
106
|
}
|
|
105
107
|
const targetRecipe = positional[0];
|
|
106
108
|
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
109
|
+
const paramAssignments = positional.slice(1);
|
|
107
110
|
if (optionFlag(options, "describe")) {
|
|
111
|
+
if (paramAssignments.length > 0) {
|
|
112
|
+
throw usageError("run --describe does not accept parameter values; inspect the declaration first.");
|
|
113
|
+
}
|
|
108
114
|
if (stream.enabled) {
|
|
109
115
|
const message = "--json-stream is for recipe execution; use --describe --json for discovery.";
|
|
110
116
|
stream.error({
|
|
@@ -116,7 +122,8 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
116
122
|
}
|
|
117
123
|
return handleDescribeRecipe(targetRecipe, options);
|
|
118
124
|
}
|
|
119
|
-
|
|
125
|
+
const params = parseRecipeParamAssignments(paramAssignments);
|
|
126
|
+
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, params, options, stream);
|
|
120
127
|
const { adapter, target } = resolveAdapter(options);
|
|
121
128
|
const json = optionFlag(options, "json");
|
|
122
129
|
const jsonOutput = json && !stream.enabled;
|
|
@@ -139,7 +146,7 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
139
146
|
}
|
|
140
147
|
if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
|
|
141
148
|
stream.phase("validate");
|
|
142
|
-
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
149
|
+
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options, params);
|
|
143
150
|
if (validated.usageError) {
|
|
144
151
|
const userAction = runUsageRecovery(
|
|
145
152
|
validated.usageError.code,
|
|
@@ -160,7 +167,7 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
160
167
|
if (validated.errorCount > 0) {
|
|
161
168
|
return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
162
169
|
}
|
|
163
|
-
const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
170
|
+
const depsBlock = adapter === "core" && await recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
164
171
|
if (depsBlock) {
|
|
165
172
|
return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
166
173
|
}
|
|
@@ -179,9 +186,11 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
179
186
|
"set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>"
|
|
180
187
|
);
|
|
181
188
|
}
|
|
189
|
+
recordCommandEvidence(artifactsDir);
|
|
182
190
|
const librarySources = validated.librarySources;
|
|
183
191
|
const runtimeOptions = {
|
|
184
192
|
...runtimeOptionsFromCli(options),
|
|
193
|
+
params,
|
|
185
194
|
...librarySources ? { librarySources } : {},
|
|
186
195
|
stdoutIsMachineContract: machine,
|
|
187
196
|
onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
|
|
@@ -372,19 +381,22 @@ function formatPreviewLine(line, out) {
|
|
|
372
381
|
const rest = match[2];
|
|
373
382
|
return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
|
|
374
383
|
}
|
|
375
|
-
function recipeUsesCoreController(recipe, librarySources) {
|
|
384
|
+
async function recipeUsesCoreController(recipe, librarySources) {
|
|
376
385
|
if (objectUsesCoreController(recipe)) return true;
|
|
377
386
|
const refs = collectCallRefs(recipe);
|
|
378
387
|
if (refs.length === 0) return false;
|
|
379
|
-
const
|
|
388
|
+
const sources = librarySources ?? [];
|
|
389
|
+
if (sources.length === 0) return false;
|
|
390
|
+
const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
|
|
391
|
+
const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
|
|
380
392
|
const visited = /* @__PURE__ */ new Set();
|
|
381
393
|
const visit = (ref) => {
|
|
382
394
|
if (visited.has(ref)) return false;
|
|
383
395
|
visited.add(ref);
|
|
384
|
-
const
|
|
385
|
-
if (!
|
|
386
|
-
if (objectUsesCoreController(
|
|
387
|
-
return collectCallRefs(
|
|
396
|
+
const dependency = recipes.get(ref);
|
|
397
|
+
if (!dependency) return false;
|
|
398
|
+
if (objectUsesCoreController(dependency.document)) return true;
|
|
399
|
+
return collectCallRefs(dependency.document).some(visit);
|
|
388
400
|
};
|
|
389
401
|
return refs.some(visit);
|
|
390
402
|
}
|
|
@@ -408,41 +420,16 @@ function collectCallRefs(value) {
|
|
|
408
420
|
visit(value);
|
|
409
421
|
return [...new Set(refs)];
|
|
410
422
|
}
|
|
411
|
-
function loadFlowCatalogs(librarySources) {
|
|
412
|
-
const flows = /* @__PURE__ */ new Map();
|
|
413
|
-
for (const source of librarySources) {
|
|
414
|
-
const flowsDir = path.join(source.root, "flows");
|
|
415
|
-
let entries;
|
|
416
|
-
try {
|
|
417
|
-
entries = fs.readdirSync(flowsDir);
|
|
418
|
-
} catch {
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
for (const entry of entries) {
|
|
422
|
-
if (!entry.endsWith(".json")) continue;
|
|
423
|
-
try {
|
|
424
|
-
const parsed = JSON.parse(fs.readFileSync(path.join(flowsDir, entry), "utf8"));
|
|
425
|
-
if (!isRecord(parsed) || !isRecord(parsed.flows)) continue;
|
|
426
|
-
for (const [id, flow] of Object.entries(parsed.flows)) {
|
|
427
|
-
if (!flows.has(id)) flows.set(id, flow);
|
|
428
|
-
}
|
|
429
|
-
} catch {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return flows;
|
|
435
|
-
}
|
|
436
423
|
function actionUsesCoreController(action) {
|
|
437
424
|
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
438
425
|
}
|
|
439
|
-
async function handleRunPlan(recipeArg, options, stream) {
|
|
426
|
+
async function handleRunPlan(recipeArg, params, options, stream) {
|
|
440
427
|
const json = optionFlag(options, "json");
|
|
441
428
|
const jsonOutput = json && !stream.enabled;
|
|
442
429
|
const { adapter, target } = resolveAdapter(options);
|
|
443
430
|
stream.phase("resolve", { adapter, target, recipe: recipeArg });
|
|
444
431
|
stream.phase("validate");
|
|
445
|
-
const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
|
|
432
|
+
const validated = await validateRunRecipeStatic(recipeArg, adapter, options, params);
|
|
446
433
|
if (validated.usageError) {
|
|
447
434
|
const userAction = runUsageRecovery(
|
|
448
435
|
validated.usageError.code,
|
|
@@ -460,7 +447,7 @@ async function handleRunPlan(recipeArg, options, stream) {
|
|
|
460
447
|
userAction
|
|
461
448
|
);
|
|
462
449
|
}
|
|
463
|
-
const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
|
|
450
|
+
const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid, effectiveParams } = validated;
|
|
464
451
|
const status = errorCount === 0 ? "pass" : "fail";
|
|
465
452
|
const nodeCount = countRecipeNodes(recipe);
|
|
466
453
|
const artifactsDir = optionString(options, "artifactsDir");
|
|
@@ -519,6 +506,7 @@ async function handleRunPlan(recipeArg, options, stream) {
|
|
|
519
506
|
status,
|
|
520
507
|
adapter,
|
|
521
508
|
recipe: recipeFile,
|
|
509
|
+
params: redactStructuredValue(effectiveParams),
|
|
522
510
|
findings,
|
|
523
511
|
plan
|
|
524
512
|
};
|
package/dist/commands/shared.js
CHANGED
|
@@ -53,7 +53,7 @@ function flag(options, key) {
|
|
|
53
53
|
return options[key] === true;
|
|
54
54
|
}
|
|
55
55
|
function targetOf(options) {
|
|
56
|
-
return path.resolve(str(options, "target") ??
|
|
56
|
+
return path.resolve(str(options, "target") ?? process.cwd());
|
|
57
57
|
}
|
|
58
58
|
const ADAPTER_TOKENS = ["mobile", "extension", "core"];
|
|
59
59
|
function resolveAdapter(options, target, hint) {
|
package/dist/heal-bounds.js
CHANGED
|
@@ -7,6 +7,10 @@ import { EXIT } from "./commands/shared.js";
|
|
|
7
7
|
function newHealState() {
|
|
8
8
|
return { recovered: [], mutations: [], attemptedRecoveries: [] };
|
|
9
9
|
}
|
|
10
|
+
function conciseFailureForHuman(output) {
|
|
11
|
+
const errors = output.split(/\r?\n/u).map((line) => /^(?:[A-Za-z]*Error):\s*(.+)$/u.exec(line.trim())?.[1]).filter((line) => Boolean(line));
|
|
12
|
+
return errors.at(-1) ?? output.trim();
|
|
13
|
+
}
|
|
10
14
|
function parseHeal(options, fallback) {
|
|
11
15
|
const value = options.heal;
|
|
12
16
|
if (value === void 0) return fallback;
|
|
@@ -113,6 +117,7 @@ export {
|
|
|
113
117
|
RECOVERY_CODE,
|
|
114
118
|
checkHealBounds,
|
|
115
119
|
classifyFailure,
|
|
120
|
+
conciseFailureForHuman,
|
|
116
121
|
ensureOverlay,
|
|
117
122
|
newHealState,
|
|
118
123
|
parseHeal,
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -150,54 +150,45 @@ Example (real actions; run mm-harness actions for this checkout's full set):
|
|
|
150
150
|
mm-harness call navigate page=perps --adapter mobile
|
|
151
151
|
mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
|
|
152
152
|
},
|
|
153
|
-
{
|
|
154
|
-
name: "flows",
|
|
155
|
-
summary: "Browse reusable parameterized action sequences.",
|
|
156
|
-
example: "mm-harness flows",
|
|
157
|
-
helpText: `mm-harness flows [list|describe <ref>|promote] [flags]
|
|
158
|
-
|
|
159
|
-
Browse reusable parameterized action sequences. Flows resolve by precedence
|
|
160
|
-
(personal > team > canonical); the highest-tier copy wins and shadows lower ones.
|
|
161
|
-
Flow resolution is adapter-global, so there is no --adapter flag here.
|
|
162
|
-
|
|
163
|
-
--library <name=path> Add/override a library source (repeatable)
|
|
164
|
-
--target <path> Checkout path (default: cwd)
|
|
165
|
-
--json Machine-readable output
|
|
166
|
-
|
|
167
|
-
Example:
|
|
168
|
-
mm-harness flows
|
|
169
|
-
mm-harness flows describe perps.clean_market_testnet
|
|
170
|
-
mm-harness flows describe perps.clean_market_testnet --json`
|
|
171
|
-
},
|
|
172
153
|
{
|
|
173
154
|
name: "run",
|
|
174
155
|
summary: "Execute a complete proof and write its evidence.",
|
|
175
156
|
example: "mm-harness run recipe.json",
|
|
176
|
-
helpText: `mm-harness run <recipe.json> [flags]
|
|
157
|
+
helpText: `mm-harness run <recipe|recipe.json> [key=value ...] [flags]
|
|
177
158
|
|
|
178
159
|
Validate and execute a complete proof, then write summary, trace, diagnostics,
|
|
179
|
-
and artifacts.
|
|
160
|
+
and artifacts. Inside a checkout, the adapter, target, ports, and artifact
|
|
161
|
+
directory are normally detected automatically.
|
|
180
162
|
|
|
163
|
+
Discovery:
|
|
181
164
|
--list List complete recipes accepted by run for this adapter; no <recipe> needed
|
|
182
|
-
--describe Show one recipe's
|
|
165
|
+
--describe Show one recipe's parameters, composed actions/recipes, and runnable command
|
|
183
166
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
167
|
+
|
|
168
|
+
Common overrides:
|
|
184
169
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
|
|
185
|
-
--cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
186
|
-
--launch-existing-dist Extension: reuse/launch the existing dist without rebuilding
|
|
187
170
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
171
|
+
--platform <ios|android> Mobile platform preference; use --adapter for the product adapter
|
|
188
172
|
--target <path> Checkout path (default: cwd)
|
|
189
173
|
--artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
|
|
190
|
-
--action-manifest <path> Override the action manifest
|
|
191
174
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
192
175
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
193
176
|
--json Machine-readable output
|
|
194
177
|
--json-stream Line-flushed JSONL progress + terminal event
|
|
195
178
|
--record-video=full-run Record a video of the run
|
|
196
179
|
|
|
180
|
+
Advanced integrations:
|
|
181
|
+
runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
|
|
182
|
+
--validation-runtime-dir, --launch-existing-dist
|
|
183
|
+
managed trust boundary (not needed for normal runs; Farmslot supplies it):
|
|
184
|
+
--source-trust, --source-kind, --source-name, --source-digest,
|
|
185
|
+
--approve-plan (binds approval to the exact reviewed plan)
|
|
186
|
+
schema: --action-manifest
|
|
187
|
+
|
|
197
188
|
Example:
|
|
198
189
|
mm-harness run wallet.smoke --describe
|
|
199
|
-
mm-harness run
|
|
200
|
-
mm-harness run recipe.json --adapter extension`
|
|
190
|
+
mm-harness run perps.clean-market-testnet market=BTC --plan
|
|
191
|
+
mm-harness run recipe.json market=ETH --adapter extension`
|
|
201
192
|
},
|
|
202
193
|
{
|
|
203
194
|
name: "last",
|
|
@@ -486,9 +477,10 @@ Example:
|
|
|
486
477
|
name: "fixtures",
|
|
487
478
|
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
|
|
488
479
|
example: "mm-harness fixtures set",
|
|
489
|
-
helpText: `mm-harness fixtures <init|sync|set|generate|finalize> [flags]
|
|
480
|
+
helpText: `mm-harness fixtures [<init|sync|set|generate|finalize>] [flags]
|
|
490
481
|
|
|
491
482
|
Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
|
|
483
|
+
With no action, show the current fixture status and safe next command.
|
|
492
484
|
init Create it from --from <path>, or explicitly choose --dev for a
|
|
493
485
|
disposable public test wallet that must never hold real funds.
|
|
494
486
|
sync Refresh the wallet fixture files on the target.
|
|
@@ -556,8 +548,8 @@ const HELP_GROUPS = [
|
|
|
556
548
|
},
|
|
557
549
|
{
|
|
558
550
|
title: "DISCOVER",
|
|
559
|
-
blurb: "
|
|
560
|
-
commands: ["actions", "call"
|
|
551
|
+
blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
|
|
552
|
+
commands: ["actions", "call"]
|
|
561
553
|
},
|
|
562
554
|
{
|
|
563
555
|
title: "PROVE",
|
package/dist/run-diagnostics.js
CHANGED
|
@@ -180,7 +180,7 @@ function runMobileIssueCommand(projectRoot, command) {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
function redactPreview(value) {
|
|
183
|
-
return value.replace(/\b(Bearer)\s+\S+/giu, "$1 [REDACTED]").replace(/\b(password|passphrase|mnemonic|seed(?:Phrase)?|privateKey|secret|token|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/giu, "$1=[REDACTED]").replace(/\b(?:0x)?[a-f0-9]{64,}\b/giu, "[REDACTED_HEX]").replace(/(https?:\/\/[^\s?]+)\?\S+/giu, "$1?[REDACTED_QUERY]").slice(0, MAX_PREVIEW_CHARS);
|
|
183
|
+
return value.replace(/\b(Bearer)\s+\S+/giu, "$1 [REDACTED]").replace(/\b(api[-_]?key|password|passphrase|mnemonic|seed(?:Phrase)?|privateKey|secret|token|authorization|vault)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/giu, "$1=[REDACTED]").replace(/\b(?:0x)?[a-f0-9]{64,}\b/giu, "[REDACTED_HEX]").replace(/(https?:\/\/[^\s?]+)\?\S+/giu, "$1?[REDACTED_QUERY]").slice(0, MAX_PREVIEW_CHARS);
|
|
184
184
|
}
|
|
185
185
|
function dedupeFindings(findings) {
|
|
186
186
|
const byFingerprint = /* @__PURE__ */ new Map();
|
package/dist/run-recording.js
CHANGED
|
@@ -10,7 +10,7 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
|
|
|
10
10
|
if (!options.record) return void 0;
|
|
11
11
|
if (process.platform !== "darwin") {
|
|
12
12
|
console.error(
|
|
13
|
-
"WARN: --record uses capture-helper and is currently supported only on macOS; continuing without video."
|
|
13
|
+
"WARN: --record-video uses capture-helper and is currently supported only on macOS; continuing without video."
|
|
14
14
|
);
|
|
15
15
|
return void 0;
|
|
16
16
|
}
|
package/dist/runner.js
CHANGED
|
@@ -69,7 +69,10 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
69
69
|
(entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
|
|
70
70
|
);
|
|
71
71
|
const autoHudDisabled = options.autoHud === false || process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false";
|
|
72
|
-
const logger =
|
|
72
|
+
const logger = deduplicateLibraryResolutionLogs(
|
|
73
|
+
options.quietStdout ? new console.Console(process.stderr) : console,
|
|
74
|
+
options.suppressLibraryResolutionLogs
|
|
75
|
+
);
|
|
73
76
|
return createRecipeRunner({
|
|
74
77
|
actionManifest,
|
|
75
78
|
adapters: withActionProgress(
|
|
@@ -101,6 +104,25 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
101
104
|
}
|
|
102
105
|
});
|
|
103
106
|
}
|
|
107
|
+
function deduplicateLibraryResolutionLogs(logger, suppress = false) {
|
|
108
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109
|
+
return {
|
|
110
|
+
info(message) {
|
|
111
|
+
if (message.startsWith("Recipe libraries:")) {
|
|
112
|
+
if (suppress) return;
|
|
113
|
+
if (seen.has(message)) return;
|
|
114
|
+
seen.add(message);
|
|
115
|
+
}
|
|
116
|
+
logger.info(message);
|
|
117
|
+
},
|
|
118
|
+
warn(message) {
|
|
119
|
+
logger.warn(message);
|
|
120
|
+
},
|
|
121
|
+
error(message) {
|
|
122
|
+
logger.error(message);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
104
126
|
async function prepareLiveAdapterImplementations(platform, actions, sources, trustTaskActions = false) {
|
|
105
127
|
const prepared = /* @__PURE__ */ new Map();
|
|
106
128
|
if (!sources) return prepared;
|
|
@@ -165,7 +187,7 @@ function withActionProgress(adapters, onActionEvent) {
|
|
|
165
187
|
try {
|
|
166
188
|
const result = await entry.execute(node, context);
|
|
167
189
|
if (reportProgress) {
|
|
168
|
-
onActionEvent({ ...event, status:
|
|
190
|
+
onActionEvent({ ...event, status: "passed" });
|
|
169
191
|
}
|
|
170
192
|
return result;
|
|
171
193
|
} catch (error) {
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -11,7 +11,7 @@ bin/mm-harness
|
|
|
11
11
|
-> @farmslot/recipe-harness generic execution, UI transports, evidence
|
|
12
12
|
-> @farmslot/protocol schemas
|
|
13
13
|
-> adapters/ focused host/browser/device leaves
|
|
14
|
-
-> library/ MetaMask actions
|
|
14
|
+
-> library/ MetaMask actions and recipes
|
|
15
15
|
-> MetaMask checkout product under test
|
|
16
16
|
```
|
|
17
17
|
|
|
@@ -37,8 +37,7 @@ task-local.
|
|
|
37
37
|
| `adapters/<platform>/` | focused shell/Node host operations |
|
|
38
38
|
| `adapters/<platform>/inject*` | assemble the checkout overlay from shipped adapter/library files |
|
|
39
39
|
| `library/actions/` | executable MetaMask action adapters |
|
|
40
|
-
| `library/
|
|
41
|
-
| `library/recipes/` | reusable proofs |
|
|
40
|
+
| `library/recipes/` | reusable parameterized behavior and proofs |
|
|
42
41
|
| `library/manifests/` | declared capability surface |
|
|
43
42
|
| `scripts/` | build/validation tooling and shipped completion/human-QA helpers |
|
|
44
43
|
|
package/docs/QA.md
CHANGED
package/docs/RECIPES.md
CHANGED
|
@@ -2,98 +2,84 @@
|
|
|
2
2
|
|
|
3
3
|
Use the smallest durable layer that proves the task:
|
|
4
4
|
|
|
5
|
-
1.
|
|
6
|
-
2.
|
|
7
|
-
3. a task-local recipe;
|
|
8
|
-
4. a
|
|
9
|
-
stable action exists.
|
|
5
|
+
1. call an existing action;
|
|
6
|
+
2. compose existing recipes;
|
|
7
|
+
3. write a task-local recipe;
|
|
8
|
+
4. use a verified controller or CDP path when no stable action exists.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Discover before authoring:
|
|
12
11
|
|
|
13
12
|
```bash
|
|
14
13
|
mm-harness actions <intent>
|
|
15
14
|
mm-harness actions --action <name>
|
|
16
|
-
mm-harness flows
|
|
17
|
-
mm-harness flows describe <ref> --json
|
|
18
15
|
mm-harness run --list
|
|
19
16
|
mm-harness run <name> --describe
|
|
20
17
|
```
|
|
21
18
|
|
|
22
|
-
Discovery reports
|
|
23
|
-
Use `--json`
|
|
19
|
+
Discovery reports parameters, selected adapter variant, nested recipes, source,
|
|
20
|
+
and shadows. Use `--json` for exact metadata.
|
|
24
21
|
|
|
25
|
-
##
|
|
22
|
+
## Author and run
|
|
26
23
|
|
|
27
|
-
An action performs one operation. A
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Minimal recipe:
|
|
24
|
+
An action performs one typed operation. A recipe owns a reusable graph or a
|
|
25
|
+
task proof and may call other recipes.
|
|
31
26
|
|
|
32
27
|
```json
|
|
33
28
|
{
|
|
34
|
-
"
|
|
35
|
-
"title": "Wallet readiness
|
|
29
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
30
|
+
"title": "Wallet readiness",
|
|
36
31
|
"description": "Proves the running wallet is reachable and unlocked.",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
"paramsSchema": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"account": { "type": "string", "default": "Account 1" }
|
|
36
|
+
},
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
},
|
|
39
|
+
"workflow": {
|
|
40
|
+
"entry": "unlock",
|
|
41
|
+
"nodes": {
|
|
42
|
+
"unlock": {
|
|
43
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
44
|
+
"intent": "Make the fixture-backed wallet ready for use",
|
|
45
|
+
"next": "done"
|
|
46
|
+
},
|
|
47
|
+
"done": { "action": "end", "status": "pass" }
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Defaults are applied before validation. Root values use `key=value`; a nested
|
|
54
|
+
recipe call uses `action: "call"`, `ref`, and `params`.
|
|
54
55
|
|
|
55
56
|
```bash
|
|
56
|
-
mm-harness run recipe.json --plan
|
|
57
|
-
mm-harness run recipe.json
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
To reuse an existing Extension build on a specific CDP runtime:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
mm-harness run recipe.json --cdp-port 9500 --launch-existing-dist
|
|
57
|
+
mm-harness run recipe.json account="Account 2" --plan
|
|
58
|
+
mm-harness run recipe.json account="Account 2"
|
|
64
59
|
```
|
|
65
60
|
|
|
66
61
|
Rules:
|
|
67
62
|
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
claims in the task recipe and screenshots.
|
|
74
|
-
- Parameterize repeated behavior instead of multiplying action names.
|
|
75
|
-
- Every `ensure_*` flow must prove a postcondition.
|
|
63
|
+
- Validate with `--plan` before side effects.
|
|
64
|
+
- Use real product paths; never mutate hidden state to fabricate proof.
|
|
65
|
+
- Prove preparation with an independent read/assert or visible UI postcondition.
|
|
66
|
+
- Keep ticket-specific claims in task-local recipes.
|
|
67
|
+
- Parameterize repeated behavior instead of multiplying names.
|
|
76
68
|
- Keep secrets out of recipes, libraries, and evidence.
|
|
77
69
|
|
|
78
|
-
The
|
|
70
|
+
The protocol is authoritative:
|
|
79
71
|
<https://farmslot.io/docs/reference/recipe-protocol-v1>.
|
|
80
72
|
|
|
81
|
-
## Share
|
|
82
|
-
|
|
83
|
-
A library may contain manifests, action implementations, flows, and complete
|
|
84
|
-
recipes:
|
|
73
|
+
## Share a library
|
|
85
74
|
|
|
86
75
|
```text
|
|
87
76
|
team-recipes/
|
|
88
77
|
library.json
|
|
89
78
|
manifests/extension.action-manifest.json
|
|
90
79
|
actions/extension/wallet/ensure_ready.mjs
|
|
91
|
-
flows/wallet.flows.json
|
|
92
80
|
recipes/onboarding/smoke.extension.recipe.json
|
|
93
81
|
```
|
|
94
82
|
|
|
95
|
-
Minimal `library.json`:
|
|
96
|
-
|
|
97
83
|
```json
|
|
98
84
|
{
|
|
99
85
|
"kind": "recipe-library",
|
|
@@ -103,59 +89,34 @@ Minimal `library.json`:
|
|
|
103
89
|
}
|
|
104
90
|
```
|
|
105
91
|
|
|
106
|
-
Enable it:
|
|
107
|
-
|
|
108
92
|
```bash
|
|
109
93
|
export RECIPE_LIBRARY_PATH="wallet=$HOME/shared-library/wallet-team"
|
|
110
|
-
mm-harness flows
|
|
111
94
|
mm-harness run --list
|
|
112
95
|
mm-harness run onboarding.smoke --describe
|
|
113
96
|
mm-harness run onboarding.smoke --plan
|
|
114
97
|
```
|
|
115
98
|
|
|
116
|
-
Use `--library wallet=/path/to/library` for one command.
|
|
117
|
-
library,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
reviewable.
|
|
121
|
-
|
|
122
|
-
Promote a repeated flow only after it has a useful description, typed params,
|
|
123
|
-
and successful proof:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
mm-harness flows promote --from recipe.json --flow wallet.ensure_ready --run artifacts
|
|
127
|
-
```
|
|
99
|
+
Use `--library wallet=/path/to/library` for one command. Resolution follows the
|
|
100
|
+
explicit library order, then bundled MetaMask. Adapter-specific variants are
|
|
101
|
+
selected deterministically. Every run records the root recipe, exact resolved
|
|
102
|
+
dependency documents, their digests, call edges, selected sources, and shadows.
|
|
128
103
|
|
|
129
104
|
## Direct product access
|
|
130
105
|
|
|
131
|
-
The bundled
|
|
132
|
-
CDP path when that is clearer than adding a permanent wrapper.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
- whether it reads or mutates state;
|
|
138
|
-
- required preconditions;
|
|
139
|
-
- the independent postcondition;
|
|
140
|
-
- trace redaction.
|
|
141
|
-
|
|
142
|
-
Do not infer route or controller names from memory. A direct mutation still
|
|
143
|
-
requires a separate read/assert or visible UI proof. If the operation repeats
|
|
144
|
-
across tasks, move it to a reviewed team library; promote it into the bundled
|
|
145
|
-
catalog only when it is broadly reusable and stable across supported products.
|
|
146
|
-
|
|
147
|
-
## Cross-platform expectations
|
|
148
|
-
|
|
149
|
-
Mobile and Extension use the same wallet and Perps vocabulary where both expose
|
|
150
|
-
the behavior. Core is headless: it reports fixture-backed controller inputs and
|
|
151
|
-
does not pretend to have a selected UI account or app route.
|
|
106
|
+
The bundled library is intentionally small. A task may use a verified controller
|
|
107
|
+
or CDP path when that is clearer than adding a permanent wrapper. Confirm the
|
|
108
|
+
current source path, mutation risk, preconditions, independent postcondition,
|
|
109
|
+
and redaction. Do not infer route or controller names from memory. Repeated team
|
|
110
|
+
behavior belongs in a reviewed team recipe; only broadly reusable stable
|
|
111
|
+
capabilities belong in the bundled library.
|
|
152
112
|
|
|
153
|
-
|
|
113
|
+
Mobile and Extension share vocabulary where the product supports it. Core is
|
|
114
|
+
headless and must not fabricate UI state. Inspect the current surface:
|
|
154
115
|
|
|
155
116
|
```bash
|
|
156
117
|
mm-harness actions --category wallet --json
|
|
157
118
|
mm-harness actions --category perps --json
|
|
158
|
-
mm-harness
|
|
119
|
+
mm-harness run --list --json
|
|
159
120
|
```
|
|
160
121
|
|
|
161
122
|
See [Security](SECURITY.md) before enabling custom executable actions.
|
package/library/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Recipe library
|
|
2
2
|
|
|
3
3
|
- An **action** performs one typed operation.
|
|
4
|
-
- A **
|
|
5
|
-
|
|
4
|
+
- A **recipe** is a parameterized, composable graph that may call actions and
|
|
5
|
+
other recipes.
|
|
6
6
|
|
|
7
7
|
- `actions/<adapter>/<group>/<name>.mjs` — executable action modules.
|
|
8
8
|
**Importing an action module RUNS it**: each ends with `runAdapter(main)` at top
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
- Actions load harness helpers ONLY through `actions/harness-exports.mjs` (the
|
|
14
14
|
dist-preferring bridge) — never deep-import `src/` (published installs have no
|
|
15
15
|
src/, and the entrypoint would close an import cycle through adapters.ts).
|
|
16
|
-
- `recipes/` — runnable
|
|
17
|
-
-
|
|
18
|
-
|
|
16
|
+
- `recipes/` — runnable recipes (`mm-harness run <name-or-file>`).
|
|
17
|
+
- Recipe resolution is explicit library order, then bundled MetaMask; selected
|
|
18
|
+
variants and shadows are recorded in run evidence.
|