@deeeed/metamask-harness 0.80.0 → 0.81.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 +26 -0
- package/adapters/manifest.json +14 -30
- package/adapters/terminal/launch.mjs +17 -13
- package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
- package/adapters/terminal/lib/readiness.mjs +20 -12
- package/adapters/terminal/policy.mjs +35 -0
- package/adapters/terminal/verify.mjs +10 -3
- package/adapters/terminal/wallet-host.mjs +43 -225
- package/dist/adapters/core/actions.js +4 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/network-observer.js +13 -0
- package/dist/adapters/extension/run.js +112 -5
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +1 -3
- package/dist/adapters/mobile/run.js +32 -9
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/terminal/actions.js +4 -1
- package/dist/adapters/terminal/doctor-checks.js +2 -0
- package/dist/adapters/terminal/launch.js +3 -0
- package/dist/adapters/terminal/overlay.js +8 -1
- package/dist/adapters/terminal/run.js +8 -3
- package/dist/adapters/terminal/surface.js +2 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +31 -6
- package/dist/commands/check.js +30 -6
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/self-test.js +4 -3
- package/dist/engine.js +57 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/mm-harness-cli.js +3 -3
- package/dist/performance-observation.js +1 -3
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +2 -1
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -3
- package/library/actions/core/perps/_controller.mjs +24 -28
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +7 -4
- package/library/actions/core/wallet/list_accounts.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +71 -3
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
- package/library/actions/terminal/app/launch.mjs +3 -2
- package/library/actions/terminal/platform/hud.mjs +2 -1
- package/library/actions/terminal/platform/page.mjs +2 -1
- package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
- package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
- package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
- package/library/manifests/core.action-manifest.json +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/package.json +4 -4
- package/site/assets/help-recipes.json +1 -0
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/commands/call.js +0 -671
- package/dist/commands/list-executables.js +0 -245
- package/dist/commands/manifest.js +0 -477
- package/dist/commands/run-engine.js +0 -1301
- package/dist/commands/run.js +0 -730
- package/dist/discovery.js +0 -15
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
|
@@ -1,1301 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { mkdtemp } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { resolveActionManifest, validateManifest } from "../manifest.js";
|
|
6
|
-
import { getAdapterSurface } from "../adapters/surface.js";
|
|
7
|
-
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
8
|
-
import { importRecipeHarness, importRecipeProtocol, runnerDir } from "../paths.js";
|
|
9
|
-
import {
|
|
10
|
-
resolveMetaMaskParameterValue,
|
|
11
|
-
validateMetaMaskActionInputs
|
|
12
|
-
} from "../metamask-action-validation.js";
|
|
13
|
-
import { beginRunDiagnostics, captureExecutionProvenance, captureHelperSupportsRecordSessionSnapshots, executedBrowser, executionProvenanceDrift, finishRunDiagnostics, ProvenanceDriftError, recipeCdpPorts, recipeEnvName, startRecipeRecording, stopRecipeRecording, validateRuntimeProofPlan, writeExecutionProvenance, gitLibraryProvenance, recipeRuntimePath, ensureOverlay, newHealState, parseHeal, recipeRunning, checkHealBounds, conciseFailureForHuman, EXIT, actionManifestPathOption, optionString, optionFlag, optionStrings, isRecord, shellQuoteArg, targetPath, usageError } from "@farmslot/recipe-cli/harness";
|
|
14
|
-
import { runnableLibraryRecipes } from "../discovery.js";
|
|
15
|
-
import { recipeComposition } from "@farmslot/recipe-cli";
|
|
16
|
-
import { consoleClassifier } from "../console-classifier.js";
|
|
17
|
-
import {
|
|
18
|
-
authorizeTrustedMutationPlan,
|
|
19
|
-
loadTrustedMutationBase
|
|
20
|
-
} from "../funding-execution-context.js";
|
|
21
|
-
const RUNNER_INCLUDES = ["src", "adapters", "library", "bin", "dist", "package.json"];
|
|
22
|
-
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution, effectsState = newHealState()) {
|
|
23
|
-
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
24
|
-
adapter,
|
|
25
|
-
projectRoot,
|
|
26
|
-
runtimeOptions
|
|
27
|
-
);
|
|
28
|
-
try {
|
|
29
|
-
const wasPreflighted = preflightedExecution !== void 0;
|
|
30
|
-
const execution = preflightedExecution ?? await resolveRecipeExecution(
|
|
31
|
-
adapter,
|
|
32
|
-
recipe,
|
|
33
|
-
artifactsDir,
|
|
34
|
-
projectRoot,
|
|
35
|
-
actionManifestPath,
|
|
36
|
-
runtimeOptions
|
|
37
|
-
);
|
|
38
|
-
const {
|
|
39
|
-
runner,
|
|
40
|
-
runRequest,
|
|
41
|
-
absoluteArtifactsDir,
|
|
42
|
-
useFramedExtensionRecording,
|
|
43
|
-
provenanceInput,
|
|
44
|
-
provenanceSnapshots
|
|
45
|
-
} = execution;
|
|
46
|
-
if (!wasPreflighted) await runner.preflight(runRequest);
|
|
47
|
-
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
48
|
-
await runner.preflight(runRequest);
|
|
49
|
-
const preExecute = await captureExecutionProvenance(
|
|
50
|
-
provenanceInput,
|
|
51
|
-
"pre-execute"
|
|
52
|
-
);
|
|
53
|
-
provenanceSnapshots.push(preExecute);
|
|
54
|
-
const preExecuteDrift = executionProvenanceDrift(
|
|
55
|
-
provenanceSnapshots[0],
|
|
56
|
-
preExecute
|
|
57
|
-
);
|
|
58
|
-
if (preExecuteDrift.length > 0) {
|
|
59
|
-
const provenancePath2 = await writeExecutionProvenance(
|
|
60
|
-
absoluteArtifactsDir,
|
|
61
|
-
provenanceSnapshots,
|
|
62
|
-
preExecuteDrift
|
|
63
|
-
);
|
|
64
|
-
throw new ProvenanceDriftError(provenancePath2, preExecuteDrift);
|
|
65
|
-
}
|
|
66
|
-
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
67
|
-
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
68
|
-
record: true,
|
|
69
|
-
cdpPort: runtimeOptions.cdpPort
|
|
70
|
-
}) : void 0;
|
|
71
|
-
let result;
|
|
72
|
-
let executionError;
|
|
73
|
-
try {
|
|
74
|
-
result = await runner.run(runRequest);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
executionError = error;
|
|
77
|
-
try {
|
|
78
|
-
await stopRecipeRecording(recording);
|
|
79
|
-
} catch (recordingError2) {
|
|
80
|
-
console.error(
|
|
81
|
-
`WARN: recipe and video recording both failed; preserving recipe failure: ${recordingError2 instanceof Error ? recordingError2.message : String(recordingError2)}`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
} finally {
|
|
85
|
-
const teardown = getAdapterSurface(adapter).run?.teardown;
|
|
86
|
-
if (teardown) await teardown(projectRoot, recipeRunEnv(adapter, runtimeOptions));
|
|
87
|
-
}
|
|
88
|
-
let recordingError;
|
|
89
|
-
if (executionError === void 0 && result) {
|
|
90
|
-
try {
|
|
91
|
-
await stopRecipeRecording(recording, result);
|
|
92
|
-
} catch (error) {
|
|
93
|
-
recordingError = error;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const end = await captureExecutionProvenance(provenanceInput, "end");
|
|
97
|
-
provenanceSnapshots.push(end);
|
|
98
|
-
const endDrift = executionProvenanceDrift(provenanceSnapshots[0], end);
|
|
99
|
-
const provenancePath = await writeExecutionProvenance(
|
|
100
|
-
absoluteArtifactsDir,
|
|
101
|
-
provenanceSnapshots,
|
|
102
|
-
endDrift,
|
|
103
|
-
result?.artifactManifestPath
|
|
104
|
-
);
|
|
105
|
-
if (endDrift.length > 0) {
|
|
106
|
-
throw new ProvenanceDriftError(provenancePath, endDrift, executionError);
|
|
107
|
-
}
|
|
108
|
-
if (executionError !== void 0) throw executionError;
|
|
109
|
-
if (recordingError !== void 0) throw recordingError;
|
|
110
|
-
if (!result) throw new Error("Recipe execution returned no result.");
|
|
111
|
-
const finalized = await finishRunDiagnostics(diagnosticBaseline, result, consoleClassifier);
|
|
112
|
-
persistRunEffects(
|
|
113
|
-
finalized.summaryPath,
|
|
114
|
-
finalized.artifactManifestPath,
|
|
115
|
-
effectsState
|
|
116
|
-
);
|
|
117
|
-
const browser = executedBrowser(
|
|
118
|
-
adapter,
|
|
119
|
-
projectRoot,
|
|
120
|
-
finalized.artifactManifestPath,
|
|
121
|
-
recipeCdpPorts(runRequest)
|
|
122
|
-
);
|
|
123
|
-
return browser ? { ...finalized, browser } : finalized;
|
|
124
|
-
} finally {
|
|
125
|
-
restoreRuntimeEnvironment();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
|
|
129
|
-
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
130
|
-
adapter,
|
|
131
|
-
projectRoot,
|
|
132
|
-
runtimeOptions
|
|
133
|
-
);
|
|
134
|
-
try {
|
|
135
|
-
const execution = await resolveRecipeExecution(
|
|
136
|
-
adapter,
|
|
137
|
-
recipe,
|
|
138
|
-
artifactsDir,
|
|
139
|
-
projectRoot,
|
|
140
|
-
actionManifestPath,
|
|
141
|
-
runtimeOptions
|
|
142
|
-
);
|
|
143
|
-
await execution.runner.preflight(execution.runRequest);
|
|
144
|
-
return execution;
|
|
145
|
-
} finally {
|
|
146
|
-
restoreRuntimeEnvironment();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function persistRunEffects(summaryPath, artifactManifestPath, state) {
|
|
150
|
-
for (const filePath of [summaryPath, artifactManifestPath]) {
|
|
151
|
-
let document;
|
|
152
|
-
try {
|
|
153
|
-
document = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
154
|
-
} catch {
|
|
155
|
-
document = null;
|
|
156
|
-
}
|
|
157
|
-
if (!isRecord(document)) {
|
|
158
|
-
throw new Error(`Recipe evidence file is not valid JSON: ${filePath}`);
|
|
159
|
-
}
|
|
160
|
-
document.recovered = [...state.recovered];
|
|
161
|
-
document.mutations = state.mutations.map((mutation) => ({ ...mutation }));
|
|
162
|
-
fs.writeFileSync(filePath, `${JSON.stringify(document, null, 2)}
|
|
163
|
-
`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
|
|
167
|
-
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
168
|
-
const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
|
|
169
|
-
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
170
|
-
const { resolveRecipeTrustInput } = await importRecipeHarness();
|
|
171
|
-
const trust = resolveRecipeTrustInput({
|
|
172
|
-
sourceTrust: runtimeOptions.source?.trust,
|
|
173
|
-
sourceKind: runtimeOptions.source?.kind,
|
|
174
|
-
sourceName: runtimeOptions.source?.name,
|
|
175
|
-
sourceDigest: runtimeOptions.source?.digest,
|
|
176
|
-
approvalDigest: runtimeOptions.approval?.planDigest
|
|
177
|
-
});
|
|
178
|
-
const librarySources = executionLibrarySources(
|
|
179
|
-
runtimeOptions.librarySources,
|
|
180
|
-
trust.source?.trust ?? "trusted"
|
|
181
|
-
);
|
|
182
|
-
const { manifest, actionSources } = await resolveActionManifest(
|
|
183
|
-
adapter,
|
|
184
|
-
actionManifestPath,
|
|
185
|
-
librarySources,
|
|
186
|
-
process.env[recipeEnvName("LIVE_ADAPTER_DIR")]
|
|
187
|
-
);
|
|
188
|
-
const { createMetaMaskRunner } = await import("../runner.js");
|
|
189
|
-
const recipeDocument = runtimeRecipeDocument(recipe, absoluteRecipePath);
|
|
190
|
-
const trustedMutation = await loadTrustedMutationBase({
|
|
191
|
-
fundingRequest: runtimeOptions.fundingRequest,
|
|
192
|
-
fundingPreflight: runtimeOptions.fundingPreflight,
|
|
193
|
-
fundingReleaseProof: runtimeOptions.fundingReleaseProof,
|
|
194
|
-
fundingBalanceProof: runtimeOptions.fundingBalanceProof,
|
|
195
|
-
artifactsDir: absoluteArtifactsDir,
|
|
196
|
-
projectRoot,
|
|
197
|
-
rootRecipe: recipeDocument
|
|
198
|
-
});
|
|
199
|
-
const surface = getAdapterSurface(adapter);
|
|
200
|
-
const suppressAutoHud = surface.run?.autoHud?.() === false;
|
|
201
|
-
const runnerOptions = {
|
|
202
|
-
quietStdout: runtimeOptions.stdoutIsMachineContract === true,
|
|
203
|
-
suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
|
|
204
|
-
autoHud: suppressAutoHud ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
|
|
205
|
-
onActionEvent: runtimeOptions.onActionEvent,
|
|
206
|
-
actionSources,
|
|
207
|
-
// A direct engineer invocation is the explicit trust boundary for a
|
|
208
|
-
// task-local adapter. Inherited gateway provenance remains authoritative,
|
|
209
|
-
// so an untrusted worker cannot promote itself with CLI flags.
|
|
210
|
-
trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
|
|
211
|
-
};
|
|
212
|
-
let runner = await createMetaMaskRunner(adapter, manifest, {
|
|
213
|
-
...runnerOptions,
|
|
214
|
-
trustedMutation
|
|
215
|
-
});
|
|
216
|
-
const useFramedExtensionRecording = surface.recording?.framed !== void 0 && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
217
|
-
const runRequest = {
|
|
218
|
-
...recipeDocument !== void 0 ? { recipeDocument } : absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
219
|
-
artifactsDir: absoluteArtifactsDir,
|
|
220
|
-
projectRoot,
|
|
221
|
-
inheritProcessEnv: false,
|
|
222
|
-
env: {
|
|
223
|
-
...recipeProcessEnvironment(),
|
|
224
|
-
...recipeRunEnv(adapter, runtimeOptions),
|
|
225
|
-
[recipeEnvName("ACTION_SOURCE_MAP")]: actionSourceMap(actionSources)
|
|
226
|
-
},
|
|
227
|
-
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
228
|
-
...trust,
|
|
229
|
-
...librarySources ? { librarySources } : {},
|
|
230
|
-
adapter,
|
|
231
|
-
...runtimeOptions.params ? { params: runtimeOptions.params } : {}
|
|
232
|
-
};
|
|
233
|
-
if (trustedMutation) {
|
|
234
|
-
const executionPlan = await runner.preflight(runRequest);
|
|
235
|
-
const authorizedMutation = await authorizeTrustedMutationPlan(trustedMutation, executionPlan);
|
|
236
|
-
runner = await createMetaMaskRunner(adapter, manifest, {
|
|
237
|
-
...runnerOptions,
|
|
238
|
-
trustedMutation: authorizedMutation
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
const provenanceInput = {
|
|
242
|
-
adapter,
|
|
243
|
-
projectRoot,
|
|
244
|
-
recipeDocument: recipeDocument ?? recipe,
|
|
245
|
-
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : {},
|
|
246
|
-
...librarySources ? { librarySources } : {},
|
|
247
|
-
excludedProductRoots: [
|
|
248
|
-
absoluteArtifactsDir,
|
|
249
|
-
recipeRuntimePath(projectRoot, "operations"),
|
|
250
|
-
recipeRuntimePath(projectRoot, "lock-members")
|
|
251
|
-
],
|
|
252
|
-
helperPaths: commandHelperPaths(recipeDocument ?? recipe, projectRoot),
|
|
253
|
-
runnerIncludes: RUNNER_INCLUDES
|
|
254
|
-
};
|
|
255
|
-
const startProvenance = await captureExecutionProvenance(
|
|
256
|
-
provenanceInput,
|
|
257
|
-
"start"
|
|
258
|
-
);
|
|
259
|
-
return {
|
|
260
|
-
runner,
|
|
261
|
-
absoluteArtifactsDir,
|
|
262
|
-
useFramedExtensionRecording,
|
|
263
|
-
runRequest,
|
|
264
|
-
provenanceInput,
|
|
265
|
-
provenanceSnapshots: [startProvenance]
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
function commandHelperPaths(recipe, projectRoot) {
|
|
269
|
-
const helpers = /* @__PURE__ */ new Set();
|
|
270
|
-
visitRecipeValues(recipe, (value) => {
|
|
271
|
-
if (!isRecord(value) || value.action !== "command" || typeof value.cmd !== "string") return;
|
|
272
|
-
const candidates = commandHelperTokens(value.cmd);
|
|
273
|
-
for (const token of candidates) {
|
|
274
|
-
if (/[$`{}]/u.test(token)) {
|
|
275
|
-
throw new Error(`Command helper path cannot be bound before execution: ${token}`);
|
|
276
|
-
}
|
|
277
|
-
if (!isHelperToken(token)) continue;
|
|
278
|
-
const absolute = path.isAbsolute(token) ? path.resolve(token) : path.resolve(projectRoot, token);
|
|
279
|
-
let stat;
|
|
280
|
-
try {
|
|
281
|
-
stat = fs.lstatSync(absolute);
|
|
282
|
-
} catch {
|
|
283
|
-
throw new Error(`Command helper source does not exist: ${token}`);
|
|
284
|
-
}
|
|
285
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
286
|
-
throw new Error(`Command helper source must be a regular file: ${token}`);
|
|
287
|
-
}
|
|
288
|
-
helpers.add(absolute);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
return [...helpers].sort();
|
|
292
|
-
}
|
|
293
|
-
function commandHelperTokens(command, depth = 0) {
|
|
294
|
-
if (depth > 4) {
|
|
295
|
-
throw new Error("Command helper sources exceed the supported shell nesting depth.");
|
|
296
|
-
}
|
|
297
|
-
const tokens = shellTokens(command);
|
|
298
|
-
const candidates = new Set(tokens.filter(isHelperToken));
|
|
299
|
-
for (const token of tokens) {
|
|
300
|
-
const assignment = /^(?:--)?(?:config|configuration|manifest|project|recipe|schema|tsconfig)=([^=]+)$/iu.exec(
|
|
301
|
-
token
|
|
302
|
-
);
|
|
303
|
-
const value = assignment?.[1];
|
|
304
|
-
if (value && (isHelperToken(value) || isConfigInputToken(value))) {
|
|
305
|
-
candidates.add(value);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
309
|
-
if (!isScriptInterpreter(tokens[index])) continue;
|
|
310
|
-
const option = tokens[index + 1];
|
|
311
|
-
if (isShellInterpreter(tokens[index]) && /^-[a-z]*c[a-z]*$/iu.test(option) && tokens[index + 2]) {
|
|
312
|
-
for (const nested of commandHelperTokens(tokens[index + 2], depth + 1)) {
|
|
313
|
-
candidates.add(nested);
|
|
314
|
-
}
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (!option.startsWith("-")) candidates.add(option);
|
|
318
|
-
}
|
|
319
|
-
return candidates;
|
|
320
|
-
}
|
|
321
|
-
function visitRecipeValues(value, visit) {
|
|
322
|
-
visit(value);
|
|
323
|
-
if (Array.isArray(value)) {
|
|
324
|
-
for (const child of value) visitRecipeValues(child, visit);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (!isRecord(value)) return;
|
|
328
|
-
for (const child of Object.values(value)) visitRecipeValues(child, visit);
|
|
329
|
-
}
|
|
330
|
-
function isHelperToken(token) {
|
|
331
|
-
return !token.startsWith("-") && !token.includes("=") && !/^[a-z][a-z0-9+.-]*:\/\//iu.test(token) && /^\S+\.(?:cjs|mjs|js|jsx|ts|tsx|sh|py)$/u.test(token);
|
|
332
|
-
}
|
|
333
|
-
function isConfigInputToken(token) {
|
|
334
|
-
return !token.startsWith("-") && !token.includes("=") && /^\S+\.(?:json|json5|ya?ml|toml)$/u.test(token);
|
|
335
|
-
}
|
|
336
|
-
function isScriptInterpreter(token) {
|
|
337
|
-
return /^(?:node|bash|sh|python|python3)$/u.test(path.basename(token));
|
|
338
|
-
}
|
|
339
|
-
function isShellInterpreter(token) {
|
|
340
|
-
return /^(?:bash|sh)$/u.test(path.basename(token));
|
|
341
|
-
}
|
|
342
|
-
function shellTokens(command) {
|
|
343
|
-
const tokens = [];
|
|
344
|
-
let token = "";
|
|
345
|
-
let quote = null;
|
|
346
|
-
let escaped = false;
|
|
347
|
-
const push = () => {
|
|
348
|
-
if (token) tokens.push(token);
|
|
349
|
-
token = "";
|
|
350
|
-
};
|
|
351
|
-
for (const character of command) {
|
|
352
|
-
if (escaped) {
|
|
353
|
-
token += character;
|
|
354
|
-
escaped = false;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (character === "\\" && quote !== "'") {
|
|
358
|
-
escaped = true;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (quote) {
|
|
362
|
-
if (character === quote) quote = null;
|
|
363
|
-
else token += character;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (character === "'" || character === '"') {
|
|
367
|
-
quote = character;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
if (/\s|[;&|<>]/u.test(character)) push();
|
|
371
|
-
else token += character;
|
|
372
|
-
}
|
|
373
|
-
if (quote || escaped) {
|
|
374
|
-
throw new Error("Command helper sources cannot be bound from an unterminated shell token.");
|
|
375
|
-
}
|
|
376
|
-
push();
|
|
377
|
-
return tokens;
|
|
378
|
-
}
|
|
379
|
-
function runtimeRecipeDocument(recipe, absoluteRecipePath) {
|
|
380
|
-
if (typeof recipe !== "string") return recipe;
|
|
381
|
-
if (!absoluteRecipePath || !fs.existsSync(absoluteRecipePath)) return void 0;
|
|
382
|
-
try {
|
|
383
|
-
return JSON.parse(fs.readFileSync(absoluteRecipePath, "utf8"));
|
|
384
|
-
} catch {
|
|
385
|
-
return void 0;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
function recipeProcessEnvironment() {
|
|
389
|
-
const env = { ...process.env };
|
|
390
|
-
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
391
|
-
delete env.MM_HARNESS_OPERATION_ID;
|
|
392
|
-
return env;
|
|
393
|
-
}
|
|
394
|
-
function executionLibrarySources(sources, invocationTrust) {
|
|
395
|
-
return sources?.map((source) => {
|
|
396
|
-
if (source.name === "metamask") return source;
|
|
397
|
-
return {
|
|
398
|
-
...source,
|
|
399
|
-
provenance: {
|
|
400
|
-
...source.provenance,
|
|
401
|
-
kind: source.provenance?.kind ?? "library",
|
|
402
|
-
trust: invocationTrust === "trusted" ? "trusted" : "unknown",
|
|
403
|
-
name: source.provenance?.name ?? source.name ?? path.basename(source.root)
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
const RUN_ENV_KEYS = [
|
|
409
|
-
"CDP_PORT",
|
|
410
|
-
"RECIPE_CDP_PORT",
|
|
411
|
-
"WATCHER_PORT",
|
|
412
|
-
"METRO_PORT"
|
|
413
|
-
];
|
|
414
|
-
function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
|
|
415
|
-
const surface = getAdapterSurface(adapter);
|
|
416
|
-
const previousEnv = Object.fromEntries(
|
|
417
|
-
[...RUN_ENV_KEYS, ...surface.run?.envKeys ?? []].map((key) => [key, process.env[key]])
|
|
418
|
-
);
|
|
419
|
-
const pinnedEnv = surface.run?.pinnedEnv?.();
|
|
420
|
-
surface.resolveSlotPorts(projectRoot);
|
|
421
|
-
if (pinnedEnv) {
|
|
422
|
-
for (const [key, value] of Object.entries(pinnedEnv)) {
|
|
423
|
-
restoreEnv(key, value);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (runtimeOptions.cdpPort) {
|
|
427
|
-
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
428
|
-
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
429
|
-
}
|
|
430
|
-
if (runtimeOptions.watcherPort) {
|
|
431
|
-
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
432
|
-
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
433
|
-
}
|
|
434
|
-
surface.run?.activateEnv?.(projectRoot, runtimeOptions);
|
|
435
|
-
return () => {
|
|
436
|
-
for (const [key, value] of Object.entries(previousEnv)) restoreEnv(key, value);
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
function actionSourceMap(sources) {
|
|
440
|
-
const roots = {};
|
|
441
|
-
for (const [action, source] of sources) {
|
|
442
|
-
if (source.tier === "official") continue;
|
|
443
|
-
roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(
|
|
444
|
-
path.dirname(path.dirname(source.manifestPath)),
|
|
445
|
-
"actions"
|
|
446
|
-
));
|
|
447
|
-
}
|
|
448
|
-
return JSON.stringify(roots);
|
|
449
|
-
}
|
|
450
|
-
function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
451
|
-
const base = {
|
|
452
|
-
CDP_PORT: runtimeOptions.cdpPort ?? process.env.CDP_PORT,
|
|
453
|
-
RECIPE_CDP_PORT: runtimeOptions.cdpPort ?? process.env.RECIPE_CDP_PORT,
|
|
454
|
-
FARMSLOT_SLOT_ID: runtimeOptions.slot ?? process.env.FARMSLOT_SLOT_ID,
|
|
455
|
-
SLOT_ID: runtimeOptions.slot ?? process.env.SLOT_ID,
|
|
456
|
-
PLATFORM: process.env.PLATFORM,
|
|
457
|
-
RECIPE_RN_EXPLICIT_PLATFORM: process.env.RECIPE_RN_EXPLICIT_PLATFORM
|
|
458
|
-
};
|
|
459
|
-
const childEnv = getAdapterSurface(adapter).run?.childEnv;
|
|
460
|
-
return childEnv ? childEnv(base) : base;
|
|
461
|
-
}
|
|
462
|
-
async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
|
|
463
|
-
await getAdapterSurface(adapter).run?.prepareRuntime?.(projectRoot, runtimeOptions);
|
|
464
|
-
}
|
|
465
|
-
function restoreEnv(key, value) {
|
|
466
|
-
if (value === void 0) delete process.env[key];
|
|
467
|
-
else process.env[key] = value;
|
|
468
|
-
}
|
|
469
|
-
async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySources, rootRef, params, proof = false) {
|
|
470
|
-
const protocol = await importRecipeProtocol();
|
|
471
|
-
let externalRecipeIds;
|
|
472
|
-
let libraryResolution;
|
|
473
|
-
const harness = await importRecipeHarness();
|
|
474
|
-
if (librarySources && librarySources.length > 0) {
|
|
475
|
-
libraryResolution = await harness.loadRecipeLibraries(librarySources, {
|
|
476
|
-
adapter
|
|
477
|
-
});
|
|
478
|
-
externalRecipeIds = new Set(libraryResolution.recipes.keys());
|
|
479
|
-
}
|
|
480
|
-
const validationOptions = externalRecipeIds !== void 0 ? { externalRecipeIds } : void 0;
|
|
481
|
-
const withManifest = protocol.validateRecipeWithManifest(
|
|
482
|
-
recipe,
|
|
483
|
-
manifest,
|
|
484
|
-
validationOptions
|
|
485
|
-
);
|
|
486
|
-
const findings = [
|
|
487
|
-
...withManifest.findings,
|
|
488
|
-
...withManifest.status === "valid" && params ? protocol.validateRecipeWithManifest(
|
|
489
|
-
resolveMetaMaskParameterValue(recipe, params),
|
|
490
|
-
manifest,
|
|
491
|
-
validationOptions
|
|
492
|
-
).findings : [],
|
|
493
|
-
...validateMetaMaskActionInputs(recipe, adapter, params)
|
|
494
|
-
];
|
|
495
|
-
if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
|
|
496
|
-
try {
|
|
497
|
-
const digest = protocol.digestRecipeDocument(recipe);
|
|
498
|
-
const dependencies = harness.resolveRecipeDependencies({
|
|
499
|
-
rootRef: rootRef ?? `$root:${digest}`,
|
|
500
|
-
root: recipe,
|
|
501
|
-
rootSource: {
|
|
502
|
-
kind: "recipe-file",
|
|
503
|
-
trust: "unknown",
|
|
504
|
-
name: rootRef ?? "recipe file",
|
|
505
|
-
digest
|
|
506
|
-
},
|
|
507
|
-
recipes: libraryResolution.recipes
|
|
508
|
-
});
|
|
509
|
-
if (params) {
|
|
510
|
-
harness.validateRecipeDependencyParams({
|
|
511
|
-
root: recipe,
|
|
512
|
-
params,
|
|
513
|
-
recipes: dependencies.recipes
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
for (const dependency of dependencies.recipes.values()) {
|
|
517
|
-
findings.push(
|
|
518
|
-
...protocol.validateRecipeWithManifest(
|
|
519
|
-
dependency.document,
|
|
520
|
-
manifest,
|
|
521
|
-
validationOptions
|
|
522
|
-
).findings
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
const validateDependencyInstances = (document, parentParams) => {
|
|
526
|
-
if (!isRecord(document) || !isRecord(document.workflow) || !isRecord(document.workflow.nodes)) return;
|
|
527
|
-
for (const rawNode of Object.values(document.workflow.nodes)) {
|
|
528
|
-
const node = resolveMetaMaskParameterValue(rawNode, parentParams);
|
|
529
|
-
if (!isRecord(node) || node.action !== "call" || typeof node.ref !== "string") continue;
|
|
530
|
-
const dependency = dependencies.recipes.get(protocol.normalizeRecipeRef(node.ref));
|
|
531
|
-
if (!dependency) continue;
|
|
532
|
-
const supplied = isRecord(node.params) ? node.params : {};
|
|
533
|
-
const childParams = protocol.applyRecipeParamDefaults(
|
|
534
|
-
supplied,
|
|
535
|
-
isRecord(dependency.document) ? dependency.document.paramsSchema : void 0
|
|
536
|
-
);
|
|
537
|
-
findings.push(...protocol.validateRecipeWithManifest(
|
|
538
|
-
resolveMetaMaskParameterValue(dependency.document, childParams),
|
|
539
|
-
manifest,
|
|
540
|
-
validationOptions
|
|
541
|
-
).findings);
|
|
542
|
-
findings.push(...validateMetaMaskActionInputs(dependency.document, adapter, childParams));
|
|
543
|
-
validateDependencyInstances(dependency.document, childParams);
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
|
-
validateDependencyInstances(recipe, params ?? {});
|
|
547
|
-
if (proof && !findings.some((finding) => finding.severity === "error")) {
|
|
548
|
-
findings.push(...validateRuntimeProofPlan(
|
|
549
|
-
recipe,
|
|
550
|
-
new Map([...dependencies.recipes].map(([ref, dependency]) => [ref, dependency.document]))
|
|
551
|
-
));
|
|
552
|
-
}
|
|
553
|
-
} catch (error) {
|
|
554
|
-
const resolutionError = error instanceof harness.RecipeResolutionError ? error : void 0;
|
|
555
|
-
if (!resolutionError) throw error;
|
|
556
|
-
findings.push({
|
|
557
|
-
severity: "error",
|
|
558
|
-
code: resolutionError.code,
|
|
559
|
-
path: "workflow",
|
|
560
|
-
message: `${resolutionError.message} Next: ${resolutionError.userAction}`
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
const errors = findings.filter(
|
|
565
|
-
(finding) => finding.severity === "error"
|
|
566
|
-
).length;
|
|
567
|
-
const warnings = findings.length - errors;
|
|
568
|
-
return {
|
|
569
|
-
status: errors > 0 ? "invalid" : "valid",
|
|
570
|
-
findings,
|
|
571
|
-
summary: { errors, warnings }
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
function isRecipeFile(p) {
|
|
575
|
-
try {
|
|
576
|
-
return fs.statSync(p).isFile();
|
|
577
|
-
} catch {
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
async function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
582
|
-
const direct = path.resolve(recipeArg);
|
|
583
|
-
if (isRecipeFile(direct)) return { recipeFile: direct };
|
|
584
|
-
const sources = effectiveLibrarySources(librarySources);
|
|
585
|
-
const harness = await importRecipeHarness();
|
|
586
|
-
const resolution = await harness.loadRecipeLibraries(sources, { adapter });
|
|
587
|
-
if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
|
|
588
|
-
const selected = resolution.recipes.get(recipeArg);
|
|
589
|
-
if (selected) return { recipeFile: selected.path, ref: selected.ref };
|
|
590
|
-
}
|
|
591
|
-
const available = [...resolution.recipes.keys()].sort();
|
|
592
|
-
const nonCanonical = sources.filter((source) => source.name !== "metamask");
|
|
593
|
-
const notFoundCore = nonCanonical.length > 0 ? `recipe not found: ${recipeArg} \u2014 not a file, and no recipe matched in library sources [${sources.map((source) => source.name ?? path.basename(source.root)).join(", ")}].` : `recipe not found: ${recipeArg} \u2014 not a file, and no packaged library recipe matched.`;
|
|
594
|
-
const suffix = available.length > 0 ? ` Library recipes for ${adapter}: ${available.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
|
|
595
|
-
return { notFound: notFoundCore + suffix };
|
|
596
|
-
}
|
|
597
|
-
function effectiveLibrarySources(librarySources) {
|
|
598
|
-
return librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: path.resolve(runnerDir, "library") }];
|
|
599
|
-
}
|
|
600
|
-
async function listRunnableRecipes(adapter, librarySources) {
|
|
601
|
-
return (await runnableLibraryRecipes(adapter, effectiveLibrarySources(librarySources))).map(
|
|
602
|
-
(recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
|
|
603
|
-
).sort((left, right) => left.name.localeCompare(right.name));
|
|
604
|
-
}
|
|
605
|
-
async function describeRunnableRecipe(recipeArg, adapter, librarySources) {
|
|
606
|
-
const resolved = await resolveRunRecipeArg(
|
|
607
|
-
recipeArg,
|
|
608
|
-
adapter,
|
|
609
|
-
librarySources
|
|
610
|
-
);
|
|
611
|
-
if ("notFound" in resolved) return resolved;
|
|
612
|
-
let document;
|
|
613
|
-
try {
|
|
614
|
-
document = JSON.parse(fs.readFileSync(resolved.recipeFile, "utf8"));
|
|
615
|
-
} catch (error) {
|
|
616
|
-
return {
|
|
617
|
-
unreadable: `could not read recipe ${resolved.recipeFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
if (!isRecord(document)) {
|
|
621
|
-
return {
|
|
622
|
-
unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.`
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
const harness = await importRecipeHarness();
|
|
626
|
-
const libraryResolution = await harness.loadRecipeLibraries(
|
|
627
|
-
effectiveLibrarySources(librarySources),
|
|
628
|
-
{ adapter }
|
|
629
|
-
);
|
|
630
|
-
const selected = resolved.ref ? libraryResolution.recipes.get(resolved.ref) : void 0;
|
|
631
|
-
const composition = recipeComposition(document, libraryResolution.recipes);
|
|
632
|
-
const summary = recipeSummary(
|
|
633
|
-
resolved.ref ?? recipeArg,
|
|
634
|
-
adapter,
|
|
635
|
-
document,
|
|
636
|
-
selected
|
|
637
|
-
);
|
|
638
|
-
return {
|
|
639
|
-
recipe: {
|
|
640
|
-
...summary,
|
|
641
|
-
source: selected?.source ?? "file",
|
|
642
|
-
file: selected?.file ?? resolved.recipeFile,
|
|
643
|
-
path: resolved.recipeFile,
|
|
644
|
-
...typeof document.$schema === "string" ? { $schema: document.$schema } : {},
|
|
645
|
-
actions: composition.actions,
|
|
646
|
-
nestedRecipes: composition.nestedRecipes,
|
|
647
|
-
unresolvedRecipes: composition.unresolvedRecipes
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
function recipeSummary(ref, adapter, document, selected) {
|
|
652
|
-
return {
|
|
653
|
-
name: ref,
|
|
654
|
-
source: selected?.source ?? "file",
|
|
655
|
-
file: selected?.file ?? "",
|
|
656
|
-
shadows: selected?.shadows ?? [],
|
|
657
|
-
adapter,
|
|
658
|
-
variant: selected?.adapter ?? null,
|
|
659
|
-
parameters: recipeParameters(document),
|
|
660
|
-
...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
|
|
661
|
-
...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {}
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
function recipeParameters(document) {
|
|
665
|
-
if (!isRecord(document.paramsSchema)) return [];
|
|
666
|
-
const schema = document.paramsSchema;
|
|
667
|
-
const required = new Set(
|
|
668
|
-
Array.isArray(schema.required) ? schema.required.filter(
|
|
669
|
-
(entry) => typeof entry === "string"
|
|
670
|
-
) : []
|
|
671
|
-
);
|
|
672
|
-
if (!isRecord(schema.properties)) return [];
|
|
673
|
-
return Object.entries(schema.properties).map(([name, raw]) => {
|
|
674
|
-
const property = isRecord(raw) ? raw : {};
|
|
675
|
-
return {
|
|
676
|
-
name,
|
|
677
|
-
...typeof property.type === "string" ? { type: property.type } : {},
|
|
678
|
-
required: required.has(name),
|
|
679
|
-
...Object.hasOwn(property, "default") ? { default: property.default } : {},
|
|
680
|
-
...typeof property.description === "string" ? { description: property.description } : {},
|
|
681
|
-
...Array.isArray(property.enum) ? { enum: property.enum } : {}
|
|
682
|
-
};
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
async function validateRunRecipeStatic(recipeArg, adapter, options, params = {}) {
|
|
686
|
-
let librarySources = await resolveMetaMaskLibrarySources(
|
|
687
|
-
optionStrings(options, "library")
|
|
688
|
-
);
|
|
689
|
-
const resolved = await resolveRunRecipeArg(
|
|
690
|
-
recipeArg,
|
|
691
|
-
adapter,
|
|
692
|
-
librarySources
|
|
693
|
-
);
|
|
694
|
-
const recipeFile = "recipeFile" in resolved ? resolved.recipeFile : path.resolve(recipeArg);
|
|
695
|
-
if ("recipeFile" in resolved && !resolved.ref) {
|
|
696
|
-
librarySources = await resolveMetaMaskLibrarySources(
|
|
697
|
-
optionStrings(options, "library"),
|
|
698
|
-
recipeFile
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
const empty = {
|
|
702
|
-
recipe: void 0,
|
|
703
|
-
recipeFile,
|
|
704
|
-
findings: [],
|
|
705
|
-
errorCount: 0,
|
|
706
|
-
manifestOk: false,
|
|
707
|
-
schemaValid: false,
|
|
708
|
-
effectiveParams: params,
|
|
709
|
-
...librarySources ? { librarySources } : {}
|
|
710
|
-
};
|
|
711
|
-
if ("notFound" in resolved) {
|
|
712
|
-
return {
|
|
713
|
-
...empty,
|
|
714
|
-
usageError: {
|
|
715
|
-
code: "RECIPE_NOT_FOUND",
|
|
716
|
-
message: resolved.notFound + await actionInsteadOfRecipeHint(recipeArg, adapter, options)
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
let recipe;
|
|
721
|
-
try {
|
|
722
|
-
recipe = JSON.parse(fs.readFileSync(recipeFile, "utf8"));
|
|
723
|
-
} catch (error) {
|
|
724
|
-
return {
|
|
725
|
-
...empty,
|
|
726
|
-
usageError: {
|
|
727
|
-
code: "RECIPE_UNPARSEABLE",
|
|
728
|
-
message: `recipe is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
const findings = [];
|
|
733
|
-
const { manifest } = await resolveActionManifest(
|
|
734
|
-
adapter,
|
|
735
|
-
optionString(options, "actionManifest"),
|
|
736
|
-
librarySources
|
|
737
|
-
);
|
|
738
|
-
let manifestOk = true;
|
|
739
|
-
try {
|
|
740
|
-
await validateManifest(manifest);
|
|
741
|
-
} catch (error) {
|
|
742
|
-
manifestOk = false;
|
|
743
|
-
findings.push({
|
|
744
|
-
severity: "error",
|
|
745
|
-
code: "manifest.invalid",
|
|
746
|
-
path: actionManifestPathOption(options, adapter),
|
|
747
|
-
message: error instanceof Error ? error.message : String(error)
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
let effectiveParams = params;
|
|
751
|
-
if (isRecord(recipe)) {
|
|
752
|
-
const protocol = await importRecipeProtocol();
|
|
753
|
-
effectiveParams = protocol.applyRecipeParamDefaults(
|
|
754
|
-
params,
|
|
755
|
-
recipe.paramsSchema
|
|
756
|
-
);
|
|
757
|
-
findings.push(
|
|
758
|
-
...protocol.validateRecipeParams(effectiveParams, recipe.paramsSchema).findings
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
const validation = manifestOk ? await validateRecipeAdapterAware(
|
|
762
|
-
adapter,
|
|
763
|
-
recipe,
|
|
764
|
-
manifest,
|
|
765
|
-
librarySources,
|
|
766
|
-
resolved.ref,
|
|
767
|
-
effectiveParams,
|
|
768
|
-
optionFlag(options, "proof")
|
|
769
|
-
) : {
|
|
770
|
-
status: "invalid",
|
|
771
|
-
findings: [],
|
|
772
|
-
summary: { errors: 1, warnings: 0 }
|
|
773
|
-
};
|
|
774
|
-
findings.push(...validation.findings);
|
|
775
|
-
const errorCount = findings.filter(
|
|
776
|
-
(finding) => finding.severity === "error"
|
|
777
|
-
).length;
|
|
778
|
-
return {
|
|
779
|
-
recipe,
|
|
780
|
-
recipeFile,
|
|
781
|
-
findings,
|
|
782
|
-
errorCount,
|
|
783
|
-
manifestOk,
|
|
784
|
-
schemaValid: validation.status === "valid" && errorCount === 0,
|
|
785
|
-
effectiveParams,
|
|
786
|
-
...librarySources ? { librarySources } : {}
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
790
|
-
if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
|
|
791
|
-
try {
|
|
792
|
-
const librarySources = await resolveMetaMaskLibrarySources(
|
|
793
|
-
optionStrings(options, "library")
|
|
794
|
-
);
|
|
795
|
-
const { manifest } = await resolveActionManifest(
|
|
796
|
-
adapter,
|
|
797
|
-
optionString(options, "actionManifest"),
|
|
798
|
-
librarySources
|
|
799
|
-
);
|
|
800
|
-
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
801
|
-
const actions = getRecipeActionManifestActionNames(manifest);
|
|
802
|
-
const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
|
|
803
|
-
if (matches.length !== 1) return "";
|
|
804
|
-
const device = optionString(options, "device");
|
|
805
|
-
const actionManifest = optionString(options, "actionManifest");
|
|
806
|
-
const parts = ["mm-harness", "call", matches[0]];
|
|
807
|
-
parts.push(
|
|
808
|
-
"--adapter",
|
|
809
|
-
adapter,
|
|
810
|
-
"--target",
|
|
811
|
-
shellQuoteArg(targetPath(options))
|
|
812
|
-
);
|
|
813
|
-
if (device) parts.push("--device", shellQuoteArg(device));
|
|
814
|
-
if (actionManifest)
|
|
815
|
-
parts.push("--action-manifest", shellQuoteArg(actionManifest));
|
|
816
|
-
return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
|
|
817
|
-
} catch {
|
|
818
|
-
return "";
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
async function resolveMetaMaskLibrarySources(libraryEntry, recipePath) {
|
|
822
|
-
const harness = await importRecipeHarness();
|
|
823
|
-
const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
|
|
824
|
-
const sources = await harness.resolveRecipeLibrarySources({
|
|
825
|
-
...cliEntries.length > 0 ? { cliEntries } : {},
|
|
826
|
-
...recipePath ? { recipePath } : {}
|
|
827
|
-
});
|
|
828
|
-
const canonicalRoot = path.resolve(runnerDir, "library");
|
|
829
|
-
const configuredCanonical = sources.find(
|
|
830
|
-
(source) => source.name === "metamask"
|
|
831
|
-
);
|
|
832
|
-
if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
|
|
833
|
-
throw usageError(
|
|
834
|
-
`Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
|
|
838
|
-
sources.push({ name: "metamask", root: canonicalRoot });
|
|
839
|
-
}
|
|
840
|
-
return Promise.all(
|
|
841
|
-
sources.map(async (source) => {
|
|
842
|
-
const isBundled = source.name === "metamask";
|
|
843
|
-
const detected = isBundled ? {} : await gitLibraryProvenance(source.root);
|
|
844
|
-
return {
|
|
845
|
-
...source,
|
|
846
|
-
provenance: {
|
|
847
|
-
kind: isBundled ? "bundled" : "library",
|
|
848
|
-
trust: isBundled ? "trusted" : "unknown",
|
|
849
|
-
name: source.name ?? path.basename(source.root),
|
|
850
|
-
...detected,
|
|
851
|
-
...source.provenance
|
|
852
|
-
}
|
|
853
|
-
};
|
|
854
|
-
})
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
function synthesizeOneNodeRecipe(action, args) {
|
|
858
|
-
return {
|
|
859
|
-
$schema: "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
860
|
-
title: `mm-harness call ${action}`,
|
|
861
|
-
description: `Ad-hoc single-action execution of ${action} via the real engine path (mm-harness call).`,
|
|
862
|
-
workflow: {
|
|
863
|
-
entry: "call",
|
|
864
|
-
nodes: {
|
|
865
|
-
call: {
|
|
866
|
-
...args,
|
|
867
|
-
action,
|
|
868
|
-
next: "done",
|
|
869
|
-
intent: typeof args.intent === "string" ? args.intent : `Complete the requested ${action} operation`
|
|
870
|
-
},
|
|
871
|
-
done: { action: "end", status: "pass" }
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
877
|
-
const librarySources = await resolveMetaMaskLibrarySources(void 0);
|
|
878
|
-
const { manifest } = await resolveActionManifest(
|
|
879
|
-
adapter,
|
|
880
|
-
actionManifest,
|
|
881
|
-
librarySources
|
|
882
|
-
);
|
|
883
|
-
const recipe = synthesizeOneNodeRecipe(action, args);
|
|
884
|
-
const validation = await validateRecipeAdapterAware(
|
|
885
|
-
adapter,
|
|
886
|
-
recipe,
|
|
887
|
-
manifest,
|
|
888
|
-
librarySources
|
|
889
|
-
);
|
|
890
|
-
if (validation.status === "invalid") return { status: "fail" };
|
|
891
|
-
const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-fixtures-"));
|
|
892
|
-
const recipeFile = path.join(scratch, "set.recipe.json");
|
|
893
|
-
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
894
|
-
`);
|
|
895
|
-
const result = await runRecipe(
|
|
896
|
-
adapter,
|
|
897
|
-
recipeFile,
|
|
898
|
-
path.join(scratch, "artifacts"),
|
|
899
|
-
target,
|
|
900
|
-
actionManifest,
|
|
901
|
-
{
|
|
902
|
-
librarySources,
|
|
903
|
-
stdoutIsMachineContract: true
|
|
904
|
-
}
|
|
905
|
-
);
|
|
906
|
-
return { status: result.status === "pass" ? "pass" : "fail" };
|
|
907
|
-
}
|
|
908
|
-
async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
|
|
909
|
-
if (recipeRunning(target)) {
|
|
910
|
-
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
911
|
-
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
|
|
912
|
-
if (json) {
|
|
913
|
-
console.log(
|
|
914
|
-
JSON.stringify(
|
|
915
|
-
{
|
|
916
|
-
schemaVersion: 1,
|
|
917
|
-
status: "fail",
|
|
918
|
-
recoverable: false,
|
|
919
|
-
error: { code: "RECIPE_RUNNING", message: msg, userAction }
|
|
920
|
-
},
|
|
921
|
-
null,
|
|
922
|
-
2
|
|
923
|
-
)
|
|
924
|
-
);
|
|
925
|
-
} else {
|
|
926
|
-
console.error(`\u2717 mm-harness: ${msg}
|
|
927
|
-
Next: ${userAction}`);
|
|
928
|
-
}
|
|
929
|
-
return EXIT.bounded;
|
|
930
|
-
}
|
|
931
|
-
const healValue = optionString(options, "heal");
|
|
932
|
-
const healOpts = {};
|
|
933
|
-
if (healValue !== void 0) healOpts.heal = healValue;
|
|
934
|
-
const heal = parseHeal(healOpts, "infra-only");
|
|
935
|
-
if (typeof heal !== "string") {
|
|
936
|
-
console.error(heal.error);
|
|
937
|
-
return EXIT.usage;
|
|
938
|
-
}
|
|
939
|
-
const state = newHealState();
|
|
940
|
-
const runtimeCheck = getAdapterSurface(adapter).run?.runtimeCheck?.({
|
|
941
|
-
target,
|
|
942
|
-
options,
|
|
943
|
-
heal,
|
|
944
|
-
state,
|
|
945
|
-
json,
|
|
946
|
-
appRestartAuthored: prepareOptions.appRestartAuthored,
|
|
947
|
-
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
948
|
-
});
|
|
949
|
-
prepareOptions.onPhase?.("install");
|
|
950
|
-
const ensured = await ensureOverlay(
|
|
951
|
-
adapter,
|
|
952
|
-
target,
|
|
953
|
-
heal,
|
|
954
|
-
state,
|
|
955
|
-
json
|
|
956
|
-
);
|
|
957
|
-
if (!ensured.ok) {
|
|
958
|
-
const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
|
|
959
|
-
if (json) {
|
|
960
|
-
console.log(
|
|
961
|
-
JSON.stringify(
|
|
962
|
-
{
|
|
963
|
-
schemaVersion: 1,
|
|
964
|
-
status: "fail",
|
|
965
|
-
recoverable: false,
|
|
966
|
-
mutations: state.mutations,
|
|
967
|
-
error: {
|
|
968
|
-
code: "OVERLAY_INSTALL_FAILED",
|
|
969
|
-
message: ensured.error,
|
|
970
|
-
userAction
|
|
971
|
-
}
|
|
972
|
-
},
|
|
973
|
-
null,
|
|
974
|
-
2
|
|
975
|
-
)
|
|
976
|
-
);
|
|
977
|
-
} else {
|
|
978
|
-
console.error(
|
|
979
|
-
`\u2717 overlay auto-ensure failed: ${ensured.error}
|
|
980
|
-
Next: ${userAction}`
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
return EXIT.infra;
|
|
984
|
-
}
|
|
985
|
-
prepareOptions.onPhase?.("healthcheck");
|
|
986
|
-
if (runtimeCheck) {
|
|
987
|
-
const current = await runtimeCheck();
|
|
988
|
-
if (current !== null) return current;
|
|
989
|
-
}
|
|
990
|
-
return { state, heal };
|
|
991
|
-
}
|
|
992
|
-
async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
|
|
993
|
-
const source = await import("../adapters/mobile/source-freshness.js");
|
|
994
|
-
const check = deps.check ?? (() => source.mobileSourceCheck(target));
|
|
995
|
-
const initial = check();
|
|
996
|
-
if (initial.status === "unavailable") {
|
|
997
|
-
if (json) {
|
|
998
|
-
console.log(
|
|
999
|
-
JSON.stringify(
|
|
1000
|
-
{
|
|
1001
|
-
schemaVersion: 1,
|
|
1002
|
-
status: "fail",
|
|
1003
|
-
recoverable: false,
|
|
1004
|
-
error: initial.error
|
|
1005
|
-
},
|
|
1006
|
-
null,
|
|
1007
|
-
2
|
|
1008
|
-
)
|
|
1009
|
-
);
|
|
1010
|
-
} else {
|
|
1011
|
-
console.error(`\u2717 ${initial.error.message}`);
|
|
1012
|
-
console.error(` Next: ${initial.error.userAction}`);
|
|
1013
|
-
}
|
|
1014
|
-
return EXIT.usage;
|
|
1015
|
-
}
|
|
1016
|
-
if (initial.status === "current") return null;
|
|
1017
|
-
const targetArg = JSON.stringify(path.resolve(target));
|
|
1018
|
-
const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
|
|
1019
|
-
const message = `Mobile loaded source is not current (${initial.status}).`;
|
|
1020
|
-
if (json) {
|
|
1021
|
-
console.log(
|
|
1022
|
-
JSON.stringify(
|
|
1023
|
-
{
|
|
1024
|
-
schemaVersion: 1,
|
|
1025
|
-
status: "fail",
|
|
1026
|
-
recoverable: true,
|
|
1027
|
-
mutations: state.mutations,
|
|
1028
|
-
error: { code: "MOBILE_SOURCE_NOT_LOADED", message, userAction }
|
|
1029
|
-
},
|
|
1030
|
-
null,
|
|
1031
|
-
2
|
|
1032
|
-
)
|
|
1033
|
-
);
|
|
1034
|
-
} else {
|
|
1035
|
-
console.error(`\u2717 ${message}`);
|
|
1036
|
-
console.error(` Next: ${userAction}`);
|
|
1037
|
-
}
|
|
1038
|
-
return EXIT.infra;
|
|
1039
|
-
}
|
|
1040
|
-
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
1041
|
-
const decide = deps.decide ?? (async () => {
|
|
1042
|
-
const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
|
|
1043
|
-
const port = Number(process.env.CDP_PORT);
|
|
1044
|
-
return decideExtensionReadiness(target, {
|
|
1045
|
-
...Number.isInteger(port) && port > 0 ? { cdpPort: port } : {},
|
|
1046
|
-
pageMode: "home-or-sidepanel"
|
|
1047
|
-
});
|
|
1048
|
-
});
|
|
1049
|
-
const initial = await decide();
|
|
1050
|
-
if (initial.decision === "ready") return null;
|
|
1051
|
-
if (initial.reasonCode === "release-artifact-identity-invalid" || initial.reasonCode === "release-artifact-runtime-missing" || initial.reasonCode === "release-artifact-runtime-stale") {
|
|
1052
|
-
const message = initial.reasons[0] ?? "The loaded release artifact identity is invalid.";
|
|
1053
|
-
const userAction2 = "rerun runtime-launch with the same artifact source";
|
|
1054
|
-
if (json) {
|
|
1055
|
-
console.log(JSON.stringify({
|
|
1056
|
-
schemaVersion: 1,
|
|
1057
|
-
status: "fail",
|
|
1058
|
-
recoverable: false,
|
|
1059
|
-
error: { code: "RELEASE_ARTIFACT_NOT_CURRENT", message, userAction: userAction2 }
|
|
1060
|
-
}, null, 2));
|
|
1061
|
-
} else {
|
|
1062
|
-
console.error(`\u2717 ${message}`);
|
|
1063
|
-
console.error(` Next: ${userAction2}`);
|
|
1064
|
-
}
|
|
1065
|
-
return EXIT.infra;
|
|
1066
|
-
}
|
|
1067
|
-
if (initial.reasonCode === "runtime-dist-check-failed") {
|
|
1068
|
-
const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
|
|
1069
|
-
const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
|
|
1070
|
-
if (json) {
|
|
1071
|
-
console.log(
|
|
1072
|
-
JSON.stringify(
|
|
1073
|
-
{
|
|
1074
|
-
schemaVersion: 1,
|
|
1075
|
-
status: "fail",
|
|
1076
|
-
recoverable: false,
|
|
1077
|
-
error: {
|
|
1078
|
-
code: "EXTENSION_RUNTIME_CHECK_FAILED",
|
|
1079
|
-
message,
|
|
1080
|
-
userAction: userAction2
|
|
1081
|
-
}
|
|
1082
|
-
},
|
|
1083
|
-
null,
|
|
1084
|
-
2
|
|
1085
|
-
)
|
|
1086
|
-
);
|
|
1087
|
-
} else {
|
|
1088
|
-
console.error(`\u2717 ${message}`);
|
|
1089
|
-
console.error(` Next: ${userAction2}`);
|
|
1090
|
-
}
|
|
1091
|
-
return EXIT.infra;
|
|
1092
|
-
}
|
|
1093
|
-
if (![
|
|
1094
|
-
"dist-stale",
|
|
1095
|
-
"dist-missing",
|
|
1096
|
-
"runtime-dist-stale",
|
|
1097
|
-
"runtime-dist-missing"
|
|
1098
|
-
].includes(initial.reasonCode)) {
|
|
1099
|
-
return null;
|
|
1100
|
-
}
|
|
1101
|
-
const tier = initial.decision === "build" ? "build" : "quick";
|
|
1102
|
-
const canHeal = heal === "auto" || heal === "infra-only" && tier === "quick";
|
|
1103
|
-
const targetArg = JSON.stringify(path.resolve(target));
|
|
1104
|
-
const userAction = tier === "build" ? `mm-harness launch --build --verify --adapter extension --target ${targetArg}` : `mm-harness launch --verify --adapter extension --target ${targetArg}`;
|
|
1105
|
-
if (!canHeal) {
|
|
1106
|
-
const message = `Extension proof runtime is not current (${initial.reasonCode}): ${initial.reasons[0] ?? initial.decision}`;
|
|
1107
|
-
if (json) {
|
|
1108
|
-
console.log(
|
|
1109
|
-
JSON.stringify(
|
|
1110
|
-
{
|
|
1111
|
-
schemaVersion: 1,
|
|
1112
|
-
status: "fail",
|
|
1113
|
-
recoverable: true,
|
|
1114
|
-
mutations: state.mutations,
|
|
1115
|
-
error: {
|
|
1116
|
-
code: "EXTENSION_RUNTIME_NOT_CURRENT",
|
|
1117
|
-
message,
|
|
1118
|
-
userAction
|
|
1119
|
-
}
|
|
1120
|
-
},
|
|
1121
|
-
null,
|
|
1122
|
-
2
|
|
1123
|
-
)
|
|
1124
|
-
);
|
|
1125
|
-
} else {
|
|
1126
|
-
console.error(`\u2717 ${message}`);
|
|
1127
|
-
console.error(` Next: ${userAction}`);
|
|
1128
|
-
}
|
|
1129
|
-
return EXIT.infra;
|
|
1130
|
-
}
|
|
1131
|
-
const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
|
|
1132
|
-
const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
|
|
1133
|
-
if (productConfigBlock) {
|
|
1134
|
-
const message = productConfigBlock.message;
|
|
1135
|
-
const userAction2 = productConfigBlock.userAction;
|
|
1136
|
-
if (json) {
|
|
1137
|
-
console.log(
|
|
1138
|
-
JSON.stringify(
|
|
1139
|
-
{
|
|
1140
|
-
schemaVersion: 1,
|
|
1141
|
-
status: "fail",
|
|
1142
|
-
recoverable: false,
|
|
1143
|
-
mutations: state.mutations,
|
|
1144
|
-
error: {
|
|
1145
|
-
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
1146
|
-
message,
|
|
1147
|
-
userAction: userAction2
|
|
1148
|
-
}
|
|
1149
|
-
},
|
|
1150
|
-
null,
|
|
1151
|
-
2
|
|
1152
|
-
)
|
|
1153
|
-
);
|
|
1154
|
-
} else {
|
|
1155
|
-
console.error(`\u2717 ${message}`);
|
|
1156
|
-
console.error(` Next: ${userAction2}`);
|
|
1157
|
-
}
|
|
1158
|
-
return EXIT.infra;
|
|
1159
|
-
}
|
|
1160
|
-
state.attemptedRecoveries.push(recoveryCode);
|
|
1161
|
-
deps.onRecovery?.(recoveryCode);
|
|
1162
|
-
if (!json)
|
|
1163
|
-
console.error(
|
|
1164
|
-
`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`
|
|
1165
|
-
);
|
|
1166
|
-
const launch = deps.launch ?? (async (requestedTier) => {
|
|
1167
|
-
const { launchExtension } = await import("./launch/extension.js");
|
|
1168
|
-
const { readExtensionPinnedFlags } = await import("../adapters/extension/feature-flags.js");
|
|
1169
|
-
const { fixtureFeatureFlags, remoteFlagRequestFor } = await import("../feature-flags.js");
|
|
1170
|
-
const { walletFixturePath } = await import("../paths.js");
|
|
1171
|
-
return launchExtension(
|
|
1172
|
-
target,
|
|
1173
|
-
requestedTier,
|
|
1174
|
-
false,
|
|
1175
|
-
"fullscreen",
|
|
1176
|
-
remoteFlagRequestFor(readExtensionPinnedFlags(target).pinned ?? fixtureFeatureFlags(walletFixturePath(target)))
|
|
1177
|
-
);
|
|
1178
|
-
});
|
|
1179
|
-
const result = await launch(tier);
|
|
1180
|
-
const verified = result.status === 0 ? await decide() : initial;
|
|
1181
|
-
if (result.status !== 0 || verified.decision !== "ready") {
|
|
1182
|
-
const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
|
|
1183
|
-
if (json) {
|
|
1184
|
-
console.log(
|
|
1185
|
-
JSON.stringify(
|
|
1186
|
-
{
|
|
1187
|
-
schemaVersion: 1,
|
|
1188
|
-
status: "fail",
|
|
1189
|
-
recoverable: false,
|
|
1190
|
-
attemptedRecoveries: state.attemptedRecoveries,
|
|
1191
|
-
error: {
|
|
1192
|
-
code: "EXTENSION_RUNTIME_RECOVERY_FAILED",
|
|
1193
|
-
message,
|
|
1194
|
-
userAction,
|
|
1195
|
-
originalError: result.output
|
|
1196
|
-
}
|
|
1197
|
-
},
|
|
1198
|
-
null,
|
|
1199
|
-
2
|
|
1200
|
-
)
|
|
1201
|
-
);
|
|
1202
|
-
} else {
|
|
1203
|
-
console.error(`\u2717 ${message}`);
|
|
1204
|
-
console.error(` Next: ${userAction}`);
|
|
1205
|
-
}
|
|
1206
|
-
return EXIT.infra;
|
|
1207
|
-
}
|
|
1208
|
-
state.recovered.push(recoveryCode);
|
|
1209
|
-
state.mutations.push({
|
|
1210
|
-
type: "runtime",
|
|
1211
|
-
action: tier === "build" ? "rebuilt" : "reloaded",
|
|
1212
|
-
reasonCode: initial.reasonCode
|
|
1213
|
-
});
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
|
-
function readRunFailureText(result) {
|
|
1217
|
-
try {
|
|
1218
|
-
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
1219
|
-
const entries = Array.isArray(trace.entries) ? trace.entries : [];
|
|
1220
|
-
return entries.filter(
|
|
1221
|
-
(entry) => entry && entry.ok === false && typeof entry.error === "string"
|
|
1222
|
-
).map((entry) => entry.error).join("\n").trim();
|
|
1223
|
-
} catch {
|
|
1224
|
-
return "";
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
async function executeWithHealBounds(exec, target, state) {
|
|
1228
|
-
const result = await exec();
|
|
1229
|
-
if (result.status === "pass" || result.status === "unknown") return { result, violation: null };
|
|
1230
|
-
return {
|
|
1231
|
-
result,
|
|
1232
|
-
violation: checkHealBounds(target, readRunFailureText(result), state)
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
1236
|
-
const userAction = (adapter ? getAdapterSurface(adapter).run?.violationUserAction?.(violation) : void 0) ?? violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
1237
|
-
if (json) {
|
|
1238
|
-
console.log(
|
|
1239
|
-
JSON.stringify(
|
|
1240
|
-
{
|
|
1241
|
-
schemaVersion: 1,
|
|
1242
|
-
command,
|
|
1243
|
-
status: "fail",
|
|
1244
|
-
recoverable: false,
|
|
1245
|
-
recovered: state.recovered,
|
|
1246
|
-
mutations: state.mutations,
|
|
1247
|
-
attemptedRecoveries: state.attemptedRecoveries,
|
|
1248
|
-
summaryPath: result.summaryPath,
|
|
1249
|
-
tracePath: result.tracePath,
|
|
1250
|
-
artifactManifestPath: result.artifactManifestPath,
|
|
1251
|
-
exitCode: violation.exitCode,
|
|
1252
|
-
error: {
|
|
1253
|
-
code: violation.code,
|
|
1254
|
-
message: violation.message,
|
|
1255
|
-
retryable: false,
|
|
1256
|
-
userAction,
|
|
1257
|
-
originalError: violation.originalError ?? null
|
|
1258
|
-
}
|
|
1259
|
-
},
|
|
1260
|
-
null,
|
|
1261
|
-
2
|
|
1262
|
-
)
|
|
1263
|
-
);
|
|
1264
|
-
} else {
|
|
1265
|
-
const message = violation.code === "APP_LOGIC_FAILURE" && violation.originalError ? conciseFailureForHuman(violation.originalError) : violation.message;
|
|
1266
|
-
console.error(
|
|
1267
|
-
`\u2717 mm-harness ${command}: ${message}` + (violation.originalError && violation.code !== "APP_LOGIC_FAILURE" ? `
|
|
1268
|
-
--- original failure ---
|
|
1269
|
-
${violation.originalError}` : "") + (userAction ? `
|
|
1270
|
-
Next: ${userAction}` : "")
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
|
-
return violation.exitCode;
|
|
1274
|
-
}
|
|
1275
|
-
function countRecipeNodes(recipe) {
|
|
1276
|
-
if (!isRecord(recipe)) return void 0;
|
|
1277
|
-
const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
|
|
1278
|
-
const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
|
|
1279
|
-
return nodes ? Object.keys(nodes).length : void 0;
|
|
1280
|
-
}
|
|
1281
|
-
export {
|
|
1282
|
-
activateRecipeRuntimeEnvironment,
|
|
1283
|
-
countRecipeNodes,
|
|
1284
|
-
describeRunnableRecipe,
|
|
1285
|
-
emitHealViolation,
|
|
1286
|
-
ensureExtensionProofRuntime,
|
|
1287
|
-
ensureMobileProofRuntime,
|
|
1288
|
-
executeWithHealBounds,
|
|
1289
|
-
listRunnableRecipes,
|
|
1290
|
-
persistRunEffects,
|
|
1291
|
-
preflightRecipe,
|
|
1292
|
-
prepareHeal,
|
|
1293
|
-
prepareRuntimeIfNeeded,
|
|
1294
|
-
resolveMetaMaskLibrarySources,
|
|
1295
|
-
resolveRunRecipeArg,
|
|
1296
|
-
runOneNode,
|
|
1297
|
-
runRecipe,
|
|
1298
|
-
synthesizeOneNodeRecipe,
|
|
1299
|
-
validateRecipeAdapterAware,
|
|
1300
|
-
validateRunRecipeStatic
|
|
1301
|
-
};
|