@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
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
recipeRunning,
|
|
10
10
|
checkHealBounds
|
|
11
11
|
} from "../heal-bounds.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveActionManifest, validateManifest } from "../manifest.js";
|
|
13
13
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
14
|
+
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
14
15
|
import {
|
|
15
16
|
importRecipeHarness,
|
|
16
17
|
importRecipeProtocol,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
beginRunDiagnostics,
|
|
25
26
|
finishRunDiagnostics
|
|
26
27
|
} from "../run-diagnostics.js";
|
|
27
|
-
import { EXIT } from "./shared.js";
|
|
28
|
+
import { EXIT, spawnScriptStreaming } from "./shared.js";
|
|
28
29
|
import {
|
|
29
30
|
actionManifestPathOption,
|
|
30
31
|
optionString,
|
|
@@ -33,50 +34,38 @@ import {
|
|
|
33
34
|
targetPath,
|
|
34
35
|
usageError
|
|
35
36
|
} from "./parse-args.js";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
|
|
42
|
-
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
43
|
-
if (runtimeOptions.cdpPort) {
|
|
44
|
-
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
45
|
-
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
46
|
-
}
|
|
47
|
-
if (runtimeOptions.watcherPort) {
|
|
48
|
-
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
49
|
-
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
50
|
-
}
|
|
51
|
-
if (runtimeOptions.launchExistingDist) {
|
|
52
|
-
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
37
|
+
function requireRecipeTrustResolver(harness) {
|
|
38
|
+
if (typeof harness.resolveRecipeTrustInput !== "function") {
|
|
39
|
+
throw usageError(
|
|
40
|
+
"Recipe trust enforcement requires a newer @farmslot/recipe-harness than the installed package. Next: mm-harness update"
|
|
41
|
+
);
|
|
53
42
|
}
|
|
43
|
+
return harness.resolveRecipeTrustInput;
|
|
44
|
+
}
|
|
45
|
+
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
|
|
46
|
+
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
47
|
+
adapter,
|
|
48
|
+
projectRoot,
|
|
49
|
+
runtimeOptions
|
|
50
|
+
);
|
|
54
51
|
try {
|
|
52
|
+
const execution = preflightedExecution ?? await resolveRecipeExecution(
|
|
53
|
+
adapter,
|
|
54
|
+
recipe,
|
|
55
|
+
artifactsDir,
|
|
56
|
+
projectRoot,
|
|
57
|
+
actionManifestPath,
|
|
58
|
+
runtimeOptions
|
|
59
|
+
);
|
|
60
|
+
const { runner, runRequest, absoluteArtifactsDir, useFramedExtensionRecording } = execution;
|
|
61
|
+
await runner.preflight(runRequest);
|
|
55
62
|
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
56
|
-
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
57
63
|
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
58
|
-
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
59
|
-
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
60
64
|
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
61
65
|
record: true,
|
|
62
66
|
cdpPort: runtimeOptions.cdpPort
|
|
63
67
|
}) : void 0;
|
|
64
68
|
try {
|
|
65
|
-
const manifest = loadActionManifest(adapter, actionManifestPath);
|
|
66
|
-
await validateManifest(manifest);
|
|
67
|
-
const { createMetaMaskRunner } = await import("../runner.js");
|
|
68
|
-
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
69
|
-
quietStdout: runtimeOptions.stdoutIsMachineContract === true,
|
|
70
|
-
onActionEvent: runtimeOptions.onActionEvent
|
|
71
|
-
});
|
|
72
|
-
const runRequest = {
|
|
73
|
-
recipePath: path.resolve(recipe),
|
|
74
|
-
artifactsDir: absoluteArtifactsDir,
|
|
75
|
-
projectRoot,
|
|
76
|
-
env: recipeRunEnv(adapter, runtimeOptions),
|
|
77
|
-
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
78
|
-
...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
|
|
79
|
-
};
|
|
80
69
|
let result;
|
|
81
70
|
try {
|
|
82
71
|
result = await runner.run(runRequest);
|
|
@@ -92,12 +81,136 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
92
81
|
await stopRecipeRecording(recording);
|
|
93
82
|
}
|
|
94
83
|
} finally {
|
|
84
|
+
restoreRuntimeEnvironment();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
|
|
88
|
+
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
89
|
+
adapter,
|
|
90
|
+
projectRoot,
|
|
91
|
+
runtimeOptions
|
|
92
|
+
);
|
|
93
|
+
try {
|
|
94
|
+
const execution = await resolveRecipeExecution(
|
|
95
|
+
adapter,
|
|
96
|
+
recipe,
|
|
97
|
+
artifactsDir,
|
|
98
|
+
projectRoot,
|
|
99
|
+
actionManifestPath,
|
|
100
|
+
runtimeOptions
|
|
101
|
+
);
|
|
102
|
+
await execution.runner.preflight(execution.runRequest);
|
|
103
|
+
return execution;
|
|
104
|
+
} finally {
|
|
105
|
+
restoreRuntimeEnvironment();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
|
|
109
|
+
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
110
|
+
const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
|
|
111
|
+
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
112
|
+
const resolveRecipeTrustInput = requireRecipeTrustResolver(await importRecipeHarness());
|
|
113
|
+
const trust = resolveRecipeTrustInput({
|
|
114
|
+
sourceTrust: runtimeOptions.source?.trust,
|
|
115
|
+
sourceKind: runtimeOptions.source?.kind,
|
|
116
|
+
sourceName: runtimeOptions.source?.name,
|
|
117
|
+
sourceDigest: runtimeOptions.source?.digest,
|
|
118
|
+
approvalDigest: runtimeOptions.approval?.planDigest
|
|
119
|
+
});
|
|
120
|
+
const librarySources = executionLibrarySources(
|
|
121
|
+
runtimeOptions.librarySources,
|
|
122
|
+
trust.source?.trust ?? "trusted"
|
|
123
|
+
);
|
|
124
|
+
const { manifest, actionSources } = await resolveActionManifest(
|
|
125
|
+
adapter,
|
|
126
|
+
actionManifestPath,
|
|
127
|
+
librarySources,
|
|
128
|
+
process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR
|
|
129
|
+
);
|
|
130
|
+
const { createMetaMaskRunner } = await import("../runner.js");
|
|
131
|
+
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
132
|
+
quietStdout: runtimeOptions.stdoutIsMachineContract === true,
|
|
133
|
+
autoHud: trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
|
|
134
|
+
onActionEvent: runtimeOptions.onActionEvent,
|
|
135
|
+
actionSources,
|
|
136
|
+
// A direct engineer invocation is the explicit trust boundary for a
|
|
137
|
+
// task-local adapter. Inherited gateway provenance remains authoritative,
|
|
138
|
+
// so an untrusted worker cannot promote itself with CLI flags.
|
|
139
|
+
trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
|
|
140
|
+
});
|
|
141
|
+
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
142
|
+
return {
|
|
143
|
+
runner,
|
|
144
|
+
absoluteArtifactsDir,
|
|
145
|
+
useFramedExtensionRecording,
|
|
146
|
+
runRequest: {
|
|
147
|
+
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
148
|
+
artifactsDir: absoluteArtifactsDir,
|
|
149
|
+
projectRoot,
|
|
150
|
+
inheritProcessEnv: false,
|
|
151
|
+
env: {
|
|
152
|
+
...recipeProcessEnvironment(),
|
|
153
|
+
...recipeRunEnv(adapter, runtimeOptions),
|
|
154
|
+
METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
|
|
155
|
+
},
|
|
156
|
+
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
157
|
+
...trust,
|
|
158
|
+
...librarySources ? { librarySources } : {}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function recipeProcessEnvironment() {
|
|
163
|
+
const env = { ...process.env };
|
|
164
|
+
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
165
|
+
return env;
|
|
166
|
+
}
|
|
167
|
+
function executionLibrarySources(sources, invocationTrust) {
|
|
168
|
+
return sources?.map((source) => {
|
|
169
|
+
if (source.name === "metamask" || source.provenance?.trust === "trusted") return source;
|
|
170
|
+
return {
|
|
171
|
+
...source,
|
|
172
|
+
provenance: {
|
|
173
|
+
...source.provenance,
|
|
174
|
+
kind: source.provenance?.kind ?? "library",
|
|
175
|
+
trust: invocationTrust === "trusted" ? "trusted" : "unknown",
|
|
176
|
+
name: source.provenance?.name ?? source.name ?? path.basename(source.root)
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
|
|
182
|
+
const previousCdpPort = process.env.CDP_PORT;
|
|
183
|
+
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
184
|
+
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
185
|
+
const previousMetroPort = process.env.METRO_PORT;
|
|
186
|
+
const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
|
|
187
|
+
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
188
|
+
if (runtimeOptions.cdpPort) {
|
|
189
|
+
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
190
|
+
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
191
|
+
}
|
|
192
|
+
if (runtimeOptions.watcherPort) {
|
|
193
|
+
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
194
|
+
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
195
|
+
}
|
|
196
|
+
if (runtimeOptions.launchExistingDist) {
|
|
197
|
+
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
198
|
+
}
|
|
199
|
+
return () => {
|
|
95
200
|
restoreEnv("CDP_PORT", previousCdpPort);
|
|
96
201
|
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
97
202
|
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
98
203
|
restoreEnv("METRO_PORT", previousMetroPort);
|
|
99
204
|
restoreEnv("METAMASK_RECIPE_EXTENSION_AUTOLAUNCH", previousExtensionAutolaunch);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function actionSourceMap(sources) {
|
|
208
|
+
const roots = {};
|
|
209
|
+
for (const [action, source] of sources) {
|
|
210
|
+
if (source.tier === "official") continue;
|
|
211
|
+
roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(path.dirname(path.dirname(source.manifestPath)), "actions"));
|
|
100
212
|
}
|
|
213
|
+
return JSON.stringify(roots);
|
|
101
214
|
}
|
|
102
215
|
function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
103
216
|
const base = {
|
|
@@ -169,12 +282,16 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
169
282
|
const file = path.join(source.root, "recipes", candidate);
|
|
170
283
|
if (isRecipeFile(file)) return { recipeFile: file };
|
|
171
284
|
}
|
|
285
|
+
const listedFile = recipeFileByPublicName(path.join(source.root, "recipes"), recipeArg, adapter);
|
|
286
|
+
if (listedFile) return { recipeFile: listedFile };
|
|
172
287
|
}
|
|
173
288
|
} else {
|
|
174
289
|
for (const candidate of candidates) {
|
|
175
290
|
const file = recipePath(candidate);
|
|
176
291
|
if (isRecipeFile(file)) return { recipeFile: file };
|
|
177
292
|
}
|
|
293
|
+
const listedFile = recipeFileByPublicName(recipePath(""), recipeArg, adapter);
|
|
294
|
+
if (listedFile) return { recipeFile: listedFile };
|
|
178
295
|
}
|
|
179
296
|
}
|
|
180
297
|
const packagedNames = libraryRecipeNames(adapter);
|
|
@@ -183,6 +300,155 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
183
300
|
const suffix = packagedNames.length > 0 ? ` Library recipes for ${adapter}: ${packagedNames.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
|
|
184
301
|
return { notFound: notFoundCore + suffix };
|
|
185
302
|
}
|
|
303
|
+
function readRecipeMetadata(file) {
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
306
|
+
return {
|
|
307
|
+
...typeof parsed.title === "string" && parsed.title.trim() ? { title: parsed.title.trim() } : {},
|
|
308
|
+
...typeof parsed.description === "string" && parsed.description.trim() ? { description: parsed.description.trim() } : {}
|
|
309
|
+
};
|
|
310
|
+
} catch {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function recipeNameFromFile(file, adapter) {
|
|
315
|
+
if (!file.endsWith(".recipe.json")) return void 0;
|
|
316
|
+
let name = file.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".");
|
|
317
|
+
const scope = name.split(".").pop() ?? "";
|
|
318
|
+
if (["mobile", "extension", "core"].includes(scope) && scope !== adapter) return void 0;
|
|
319
|
+
const scoped = scope === adapter;
|
|
320
|
+
if (scoped) name = name.slice(0, -(adapter.length + 1));
|
|
321
|
+
return { name, scoped };
|
|
322
|
+
}
|
|
323
|
+
function recipeFileByPublicName(recipesRoot, recipeName, adapter) {
|
|
324
|
+
let match;
|
|
325
|
+
let files;
|
|
326
|
+
try {
|
|
327
|
+
files = listRecipeFiles(recipesRoot);
|
|
328
|
+
} catch {
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
for (const file of files) {
|
|
332
|
+
const candidate = recipeNameFromFile(file, adapter);
|
|
333
|
+
if (!candidate || candidate.name !== recipeName) continue;
|
|
334
|
+
if (!match || candidate.scoped && !match.scoped) match = { file, scoped: candidate.scoped };
|
|
335
|
+
}
|
|
336
|
+
return match ? path.join(recipesRoot, match.file) : void 0;
|
|
337
|
+
}
|
|
338
|
+
function listRunnableRecipes(adapter, librarySources) {
|
|
339
|
+
const sources = librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: recipePath("") }];
|
|
340
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const source of sources) {
|
|
342
|
+
const recipesRoot = librarySources && librarySources.length > 0 ? path.join(source.root, "recipes") : source.root;
|
|
343
|
+
let files;
|
|
344
|
+
try {
|
|
345
|
+
files = listRecipeFiles(recipesRoot);
|
|
346
|
+
} catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const sourceName = source.name ?? path.basename(source.root);
|
|
350
|
+
const withinSource = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
const candidate = recipeNameFromFile(file, adapter);
|
|
353
|
+
if (!candidate) continue;
|
|
354
|
+
const existing = withinSource.get(candidate.name);
|
|
355
|
+
if (!existing || candidate.scoped && !existing.scoped) {
|
|
356
|
+
withinSource.set(candidate.name, { file, scoped: candidate.scoped });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const [name, candidate] of withinSource) {
|
|
360
|
+
const winner = resolved.get(name);
|
|
361
|
+
if (winner) winner.shadows.push(sourceName);
|
|
362
|
+
else resolved.set(name, {
|
|
363
|
+
name,
|
|
364
|
+
source: sourceName,
|
|
365
|
+
file: path.join("recipes", candidate.file),
|
|
366
|
+
shadows: [],
|
|
367
|
+
...readRecipeMetadata(path.join(recipesRoot, candidate.file))
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return [...resolved.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
372
|
+
}
|
|
373
|
+
function describeRunnableRecipe(recipeArg, adapter, librarySources) {
|
|
374
|
+
const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
|
|
375
|
+
if ("notFound" in resolved) return resolved;
|
|
376
|
+
let document;
|
|
377
|
+
try {
|
|
378
|
+
document = JSON.parse(fs.readFileSync(resolved.recipeFile, "utf8"));
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
unreadable: `could not read recipe ${resolved.recipeFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (!isRecord(document)) {
|
|
385
|
+
return { unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.` };
|
|
386
|
+
}
|
|
387
|
+
const listed = listRunnableRecipes(adapter, librarySources).find((candidate) => candidate.name === recipeArg);
|
|
388
|
+
const composition = recipeComposition(document);
|
|
389
|
+
return {
|
|
390
|
+
recipe: {
|
|
391
|
+
name: listed?.name ?? recipeArg,
|
|
392
|
+
source: listed?.source ?? "file",
|
|
393
|
+
file: listed?.file ?? resolved.recipeFile,
|
|
394
|
+
shadows: listed?.shadows ?? [],
|
|
395
|
+
...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
|
|
396
|
+
...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {},
|
|
397
|
+
adapter,
|
|
398
|
+
path: resolved.recipeFile,
|
|
399
|
+
...Object.prototype.hasOwnProperty.call(document, "schema_version") ? { schemaVersion: document.schema_version } : {},
|
|
400
|
+
inputs: isRecord(document.inputs) ? redactRecipeInputs(document.inputs) : {},
|
|
401
|
+
inputOverridesSupported: false,
|
|
402
|
+
actions: composition.actions,
|
|
403
|
+
actionsComplete: composition.actionsComplete,
|
|
404
|
+
flows: composition.flows
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function recipeComposition(recipe) {
|
|
409
|
+
const actions = /* @__PURE__ */ new Set();
|
|
410
|
+
const flows = /* @__PURE__ */ new Set();
|
|
411
|
+
const visitNode = (value) => {
|
|
412
|
+
if (!isRecord(value) || typeof value.action !== "string") return;
|
|
413
|
+
actions.add(value.action);
|
|
414
|
+
if (value.action === "call" && typeof value.ref === "string") flows.add(value.ref);
|
|
415
|
+
};
|
|
416
|
+
const visitWorkflow = (value) => {
|
|
417
|
+
if (!isRecord(value)) return;
|
|
418
|
+
for (const phase of ["setup", "teardown"]) {
|
|
419
|
+
if (Array.isArray(value[phase])) value[phase].forEach(visitNode);
|
|
420
|
+
}
|
|
421
|
+
if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
|
|
422
|
+
};
|
|
423
|
+
visitNode(recipe.startState);
|
|
424
|
+
if (isRecord(recipe.validate)) visitWorkflow(recipe.validate.workflow);
|
|
425
|
+
const inlineFlows = isRecord(recipe.flows) ? recipe.flows : {};
|
|
426
|
+
const visitedFlows = /* @__PURE__ */ new Set();
|
|
427
|
+
let actionsComplete = true;
|
|
428
|
+
for (const ref of flows) {
|
|
429
|
+
if (visitedFlows.has(ref)) continue;
|
|
430
|
+
visitedFlows.add(ref);
|
|
431
|
+
const flow = inlineFlows[ref];
|
|
432
|
+
if (flow !== void 0) {
|
|
433
|
+
visitWorkflow(isRecord(flow) && isRecord(flow.workflow) ? flow.workflow : flow);
|
|
434
|
+
} else {
|
|
435
|
+
actionsComplete = false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
actions: [...actions].sort(),
|
|
440
|
+
actionsComplete,
|
|
441
|
+
flows: [...flows].sort()
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function redactRecipeInputs(inputs) {
|
|
445
|
+
return Object.fromEntries(
|
|
446
|
+
Object.entries(inputs).map(([key, value]) => [
|
|
447
|
+
key,
|
|
448
|
+
/(?:password|passphrase|secret|token|credential|mnemonic|seed|srp|vault|api[_-]?key|private[_-]?key)/iu.test(key) ? "<redacted>" : isRecord(value) ? redactRecipeInputs(value) : Array.isArray(value) ? value.map((entry) => isRecord(entry) ? redactRecipeInputs(entry) : entry) : value
|
|
449
|
+
])
|
|
450
|
+
);
|
|
451
|
+
}
|
|
186
452
|
function libraryRecipeNames(adapter) {
|
|
187
453
|
let entries;
|
|
188
454
|
try {
|
|
@@ -190,11 +456,7 @@ function libraryRecipeNames(adapter) {
|
|
|
190
456
|
} catch {
|
|
191
457
|
return [];
|
|
192
458
|
}
|
|
193
|
-
const
|
|
194
|
-
const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".")).filter((n) => {
|
|
195
|
-
const scope = n.split(".").pop() ?? "";
|
|
196
|
-
return !scopes.includes(scope) || scope === adapter;
|
|
197
|
-
}).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
|
|
459
|
+
const names = entries.map((file) => recipeNameFromFile(file, adapter)?.name).filter((name) => Boolean(name));
|
|
198
460
|
return [...new Set(names)].sort();
|
|
199
461
|
}
|
|
200
462
|
function recipeNameCandidates(recipeArg, adapter) {
|
|
@@ -259,7 +521,11 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
259
521
|
};
|
|
260
522
|
}
|
|
261
523
|
const findings = [];
|
|
262
|
-
const manifest =
|
|
524
|
+
const { manifest } = await resolveActionManifest(
|
|
525
|
+
adapter,
|
|
526
|
+
optionString(options, "actionManifest"),
|
|
527
|
+
librarySources
|
|
528
|
+
);
|
|
263
529
|
let manifestOk = true;
|
|
264
530
|
try {
|
|
265
531
|
await validateManifest(manifest);
|
|
@@ -309,7 +575,12 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
309
575
|
async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
310
576
|
if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
|
|
311
577
|
try {
|
|
312
|
-
const
|
|
578
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
579
|
+
const { manifest } = await resolveActionManifest(
|
|
580
|
+
adapter,
|
|
581
|
+
optionString(options, "actionManifest"),
|
|
582
|
+
librarySources
|
|
583
|
+
);
|
|
313
584
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
314
585
|
const actions = getRecipeActionManifestActionNames(manifest);
|
|
315
586
|
const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
|
|
@@ -330,16 +601,33 @@ async function resolveMetaMaskLibrarySources(libraryEntry) {
|
|
|
330
601
|
if (typeof harness.resolveRecipeLibrarySources !== "function") {
|
|
331
602
|
if (libraryEntry) {
|
|
332
603
|
throw usageError(
|
|
333
|
-
"--library requires @farmslot/recipe-harness
|
|
604
|
+
"--library requires a current @farmslot/recipe-harness installation that exports recipe-library support. Next: mm-harness update"
|
|
334
605
|
);
|
|
335
606
|
}
|
|
336
607
|
return void 0;
|
|
337
608
|
}
|
|
609
|
+
const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
|
|
338
610
|
const sources = await harness.resolveRecipeLibrarySources(
|
|
339
|
-
|
|
611
|
+
cliEntries.length > 0 ? { cliEntries } : void 0
|
|
340
612
|
);
|
|
341
|
-
|
|
342
|
-
|
|
613
|
+
const canonicalRoot = path.resolve(runnerDir, "library");
|
|
614
|
+
const configuredCanonical = sources.find((source) => source.name === "metamask");
|
|
615
|
+
if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
|
|
616
|
+
throw usageError(
|
|
617
|
+
`Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
|
|
621
|
+
sources.push({ name: "metamask", root: canonicalRoot });
|
|
622
|
+
}
|
|
623
|
+
return sources.map((source) => ({
|
|
624
|
+
...source,
|
|
625
|
+
provenance: source.provenance ?? {
|
|
626
|
+
kind: source.name === "metamask" ? "bundled" : "library",
|
|
627
|
+
trust: source.name === "metamask" ? "trusted" : "unknown",
|
|
628
|
+
name: source.name ?? path.basename(source.root)
|
|
629
|
+
}
|
|
630
|
+
}));
|
|
343
631
|
}
|
|
344
632
|
function synthesizeOneNodeRecipe(action, args) {
|
|
345
633
|
return {
|
|
@@ -350,7 +638,12 @@ function synthesizeOneNodeRecipe(action, args) {
|
|
|
350
638
|
workflow: {
|
|
351
639
|
entry: "call",
|
|
352
640
|
nodes: {
|
|
353
|
-
call: {
|
|
641
|
+
call: {
|
|
642
|
+
...args,
|
|
643
|
+
action,
|
|
644
|
+
next: "done",
|
|
645
|
+
intent: typeof args.intent === "string" ? args.intent : `Call ${action} in isolation`
|
|
646
|
+
},
|
|
354
647
|
done: { action: "end", status: "pass" }
|
|
355
648
|
}
|
|
356
649
|
}
|
|
@@ -358,7 +651,8 @@ function synthesizeOneNodeRecipe(action, args) {
|
|
|
358
651
|
};
|
|
359
652
|
}
|
|
360
653
|
async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
361
|
-
const
|
|
654
|
+
const librarySources = await resolveMetaMaskLibrarySources(void 0);
|
|
655
|
+
const { manifest } = await resolveActionManifest(adapter, actionManifest, librarySources);
|
|
362
656
|
const recipe = synthesizeOneNodeRecipe(action, args);
|
|
363
657
|
const validation = await validateRecipeAdapterAware(recipe, manifest);
|
|
364
658
|
if (validation.status === "invalid") return { status: "fail" };
|
|
@@ -367,7 +661,7 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
|
367
661
|
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
368
662
|
`);
|
|
369
663
|
const result = await runRecipe(adapter, recipeFile, path.join(scratch, "artifacts"), target, actionManifest, {
|
|
370
|
-
librarySources
|
|
664
|
+
librarySources,
|
|
371
665
|
stdoutIsMachineContract: true
|
|
372
666
|
});
|
|
373
667
|
return { status: result.status === "pass" ? "pass" : "fail" };
|
|
@@ -495,21 +789,27 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
495
789
|
return { status: result2.status === "pass" ? 0 : EXIT.runtime, output: result2.status };
|
|
496
790
|
});
|
|
497
791
|
const result = await restart();
|
|
498
|
-
const
|
|
792
|
+
const platform = resolveRunMobilePlatform();
|
|
793
|
+
const bridge = result.status === 0 ? await (deps.waitForBridge ? deps.waitForBridge() : spawnScriptStreaming(
|
|
794
|
+
path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh"),
|
|
795
|
+
["--target", target, "--platform", platform],
|
|
796
|
+
target
|
|
797
|
+
)) : { status: result.status, output: result.output };
|
|
798
|
+
const recorded = result.status === 0 && bridge.status === 0 && record(initial.fingerprint);
|
|
499
799
|
const verified = recorded ? check() : initial;
|
|
500
|
-
if (result.status !== 0 || !recorded || verified.status !== "current") {
|
|
501
|
-
const message = "Mobile source restart did not produce a current loaded-source baseline.";
|
|
800
|
+
if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
|
|
801
|
+
const message = bridge.status !== 0 ? "Mobile source restart completed but the platform-matched bridge did not become ready." : "Mobile source restart did not produce a current loaded-source baseline.";
|
|
502
802
|
if (json) {
|
|
503
803
|
console.log(JSON.stringify({
|
|
504
804
|
schemaVersion: 1,
|
|
505
805
|
status: "fail",
|
|
506
806
|
recoverable: false,
|
|
507
807
|
attemptedRecoveries: state.attemptedRecoveries,
|
|
508
|
-
error: { code: "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: result.output }
|
|
808
|
+
error: { code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: bridge.output || result.output }
|
|
509
809
|
}, null, 2));
|
|
510
810
|
} else {
|
|
511
811
|
console.error(`\u2717 ${message}`);
|
|
512
|
-
console.error(` Next: ${userAction}`);
|
|
812
|
+
console.error(` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`);
|
|
513
813
|
}
|
|
514
814
|
return EXIT.infra;
|
|
515
815
|
}
|
|
@@ -517,6 +817,12 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
517
817
|
state.mutations.push({ type: "runtime", action: "restarted", reasonCode: `source-${initial.status}` });
|
|
518
818
|
return null;
|
|
519
819
|
}
|
|
820
|
+
function resolveRunMobilePlatform() {
|
|
821
|
+
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
822
|
+
return process.env.PLATFORM;
|
|
823
|
+
}
|
|
824
|
+
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
825
|
+
}
|
|
520
826
|
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
521
827
|
const decide = deps.decide ?? (async () => {
|
|
522
828
|
const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
|
|
@@ -568,6 +874,24 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
568
874
|
return EXIT.infra;
|
|
569
875
|
}
|
|
570
876
|
const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
|
|
877
|
+
const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
|
|
878
|
+
if (productConfigBlock) {
|
|
879
|
+
const message = productConfigBlock.message;
|
|
880
|
+
const userAction2 = productConfigBlock.userAction;
|
|
881
|
+
if (json) {
|
|
882
|
+
console.log(JSON.stringify({
|
|
883
|
+
schemaVersion: 1,
|
|
884
|
+
status: "fail",
|
|
885
|
+
recoverable: false,
|
|
886
|
+
mutations: state.mutations,
|
|
887
|
+
error: { code: "EXTENSION_PRODUCT_CONFIG_REQUIRED", message, userAction: userAction2 }
|
|
888
|
+
}, null, 2));
|
|
889
|
+
} else {
|
|
890
|
+
console.error(`\u2717 ${message}`);
|
|
891
|
+
console.error(` Next: ${userAction2}`);
|
|
892
|
+
}
|
|
893
|
+
return EXIT.infra;
|
|
894
|
+
}
|
|
571
895
|
state.attemptedRecoveries.push(recoveryCode);
|
|
572
896
|
deps.onRecovery?.(recoveryCode);
|
|
573
897
|
if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
|
|
@@ -690,12 +1014,16 @@ function countRecipeNodes(recipe) {
|
|
|
690
1014
|
}
|
|
691
1015
|
export {
|
|
692
1016
|
countRecipeNodes,
|
|
1017
|
+
describeRunnableRecipe,
|
|
693
1018
|
emitHealViolation,
|
|
694
1019
|
ensureExtensionProofRuntime,
|
|
695
1020
|
ensureMobileProofRuntime,
|
|
696
1021
|
executeWithHealBounds,
|
|
1022
|
+
listRunnableRecipes,
|
|
1023
|
+
preflightRecipe,
|
|
697
1024
|
prepareHeal,
|
|
698
1025
|
recoverRunInfra,
|
|
1026
|
+
requireRecipeTrustResolver,
|
|
699
1027
|
resolveMetaMaskLibrarySources,
|
|
700
1028
|
resolveRunRecipeArg,
|
|
701
1029
|
runOneNode,
|