@deeeed/metamask-harness 0.17.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1059 -0
- package/README.md +91 -140
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +7 -13
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +2 -2
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +91 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +34 -83
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- 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 +7 -18
- package/dist/command-contract.js +462 -0
- package/dist/command-journal.js +263 -0
- package/dist/commands/call.js +252 -57
- package/dist/commands/check.js +10 -4
- package/dist/commands/completion-candidates.js +17 -22
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +142 -35
- package/dist/commands/last.js +60 -0
- package/dist/commands/launch/extension.js +40 -17
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +187 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +286 -35
- package/dist/commands/parse-args.js +68 -4
- package/dist/commands/provision.js +10 -4
- package/dist/commands/run-engine.js +772 -264
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +378 -85
- package/dist/commands/shared.js +76 -4
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +7 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +146 -73
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +140 -9
- package/docs/CONTRIBUTING.md +136 -0
- package/docs/QA.md +184 -0
- package/docs/RECIPES.md +122 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +7 -3
- package/library/actions/core/perps/_controller.mjs +43 -56
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/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/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1214 -396
- package/library/manifests/extension.action-manifest.json +1635 -728
- package/library/manifests/mobile.action-manifest.json +1740 -727
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
- package/library/recipes/runner/smoke.core.recipe.json +25 -0
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
- package/package.json +12 -5
- package/scripts/completions.sh +8 -11
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -62
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
- package/library/flows/perps.flows.json +0 -64
|
@@ -7,90 +7,220 @@ import {
|
|
|
7
7
|
newHealState,
|
|
8
8
|
parseHeal,
|
|
9
9
|
recipeRunning,
|
|
10
|
-
checkHealBounds
|
|
10
|
+
checkHealBounds,
|
|
11
|
+
conciseFailureForHuman
|
|
11
12
|
} from "../heal-bounds.js";
|
|
12
|
-
import {
|
|
13
|
+
import { resolveActionManifest, validateManifest } from "../manifest.js";
|
|
13
14
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
15
|
+
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
14
16
|
import {
|
|
15
17
|
importRecipeHarness,
|
|
16
18
|
importRecipeProtocol,
|
|
17
|
-
recipePath,
|
|
18
19
|
runnerDir
|
|
19
20
|
} from "../paths.js";
|
|
20
21
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
21
|
-
import { listRecipeFiles } from "../recipe-files.js";
|
|
22
22
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
23
|
+
import {
|
|
24
|
+
beginRunDiagnostics,
|
|
25
|
+
finishRunDiagnostics
|
|
26
|
+
} from "../run-diagnostics.js";
|
|
23
27
|
import { EXIT, spawnScriptStreaming } from "./shared.js";
|
|
24
28
|
import {
|
|
25
29
|
actionManifestPathOption,
|
|
26
30
|
optionString,
|
|
31
|
+
optionStrings,
|
|
27
32
|
isRecord,
|
|
28
33
|
shellQuoteArg,
|
|
34
|
+
targetPath,
|
|
29
35
|
usageError
|
|
30
36
|
} from "./parse-args.js";
|
|
31
|
-
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
38
|
-
if (runtimeOptions.cdpPort) {
|
|
39
|
-
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
40
|
-
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
41
|
-
}
|
|
42
|
-
if (runtimeOptions.watcherPort) {
|
|
43
|
-
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
44
|
-
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
45
|
-
}
|
|
46
|
-
if (runtimeOptions.launchExistingDist) {
|
|
47
|
-
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
48
|
-
}
|
|
37
|
+
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
|
|
38
|
+
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
39
|
+
adapter,
|
|
40
|
+
projectRoot,
|
|
41
|
+
runtimeOptions
|
|
42
|
+
);
|
|
49
43
|
try {
|
|
44
|
+
const execution = preflightedExecution ?? await resolveRecipeExecution(
|
|
45
|
+
adapter,
|
|
46
|
+
recipe,
|
|
47
|
+
artifactsDir,
|
|
48
|
+
projectRoot,
|
|
49
|
+
actionManifestPath,
|
|
50
|
+
runtimeOptions
|
|
51
|
+
);
|
|
52
|
+
const {
|
|
53
|
+
runner,
|
|
54
|
+
runRequest,
|
|
55
|
+
absoluteArtifactsDir,
|
|
56
|
+
useFramedExtensionRecording
|
|
57
|
+
} = execution;
|
|
58
|
+
await runner.preflight(runRequest);
|
|
50
59
|
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
51
|
-
const
|
|
52
|
-
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
53
|
-
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
60
|
+
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
54
61
|
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
55
62
|
record: true,
|
|
56
63
|
cdpPort: runtimeOptions.cdpPort
|
|
57
64
|
}) : void 0;
|
|
58
65
|
try {
|
|
59
|
-
const manifest = loadActionManifest(adapter, actionManifestPath);
|
|
60
|
-
await validateManifest(manifest);
|
|
61
|
-
const { createMetaMaskRunner } = await import("../runner.js");
|
|
62
|
-
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
63
|
-
quietStdout: runtimeOptions.stdoutIsMachineContract === true
|
|
64
|
-
});
|
|
65
|
-
const runRequest = {
|
|
66
|
-
recipePath: path.resolve(recipe),
|
|
67
|
-
artifactsDir: absoluteArtifactsDir,
|
|
68
|
-
projectRoot,
|
|
69
|
-
env: recipeRunEnv(adapter, runtimeOptions),
|
|
70
|
-
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
71
|
-
...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
|
|
72
|
-
};
|
|
73
66
|
let result;
|
|
74
67
|
try {
|
|
75
68
|
result = await runner.run(runRequest);
|
|
76
69
|
} finally {
|
|
77
70
|
if (adapter === "mobile") {
|
|
78
71
|
const { hideMobileHudOnTeardown } = await import("../adapters.js");
|
|
79
|
-
await hideMobileHudOnTeardown(
|
|
72
|
+
await hideMobileHudOnTeardown(
|
|
73
|
+
projectRoot,
|
|
74
|
+
recipeRunEnv(adapter, runtimeOptions)
|
|
75
|
+
);
|
|
80
76
|
}
|
|
81
77
|
}
|
|
82
78
|
await stopRecipeRecording(recording, result);
|
|
83
|
-
return result;
|
|
79
|
+
return finishRunDiagnostics(diagnosticBaseline, result);
|
|
84
80
|
} finally {
|
|
85
81
|
await stopRecipeRecording(recording);
|
|
86
82
|
}
|
|
87
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 } = 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
|
+
suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
|
|
134
|
+
autoHud: trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
|
|
135
|
+
onActionEvent: runtimeOptions.onActionEvent,
|
|
136
|
+
actionSources,
|
|
137
|
+
// A direct engineer invocation is the explicit trust boundary for a
|
|
138
|
+
// task-local adapter. Inherited gateway provenance remains authoritative,
|
|
139
|
+
// so an untrusted worker cannot promote itself with CLI flags.
|
|
140
|
+
trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
|
|
141
|
+
});
|
|
142
|
+
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
143
|
+
return {
|
|
144
|
+
runner,
|
|
145
|
+
absoluteArtifactsDir,
|
|
146
|
+
useFramedExtensionRecording,
|
|
147
|
+
runRequest: {
|
|
148
|
+
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
149
|
+
artifactsDir: absoluteArtifactsDir,
|
|
150
|
+
projectRoot,
|
|
151
|
+
inheritProcessEnv: false,
|
|
152
|
+
env: {
|
|
153
|
+
...recipeProcessEnvironment(),
|
|
154
|
+
...recipeRunEnv(adapter, runtimeOptions),
|
|
155
|
+
METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
|
|
156
|
+
},
|
|
157
|
+
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
158
|
+
...trust,
|
|
159
|
+
...librarySources ? { librarySources } : {},
|
|
160
|
+
adapter,
|
|
161
|
+
...runtimeOptions.params ? { params: runtimeOptions.params } : {}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function recipeProcessEnvironment() {
|
|
166
|
+
const env = { ...process.env };
|
|
167
|
+
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
168
|
+
return env;
|
|
169
|
+
}
|
|
170
|
+
function executionLibrarySources(sources, invocationTrust) {
|
|
171
|
+
return sources?.map((source) => {
|
|
172
|
+
if (source.name === "metamask" || source.provenance?.trust === "trusted")
|
|
173
|
+
return source;
|
|
174
|
+
return {
|
|
175
|
+
...source,
|
|
176
|
+
provenance: {
|
|
177
|
+
...source.provenance,
|
|
178
|
+
kind: source.provenance?.kind ?? "library",
|
|
179
|
+
trust: invocationTrust === "trusted" ? "trusted" : "unknown",
|
|
180
|
+
name: source.provenance?.name ?? source.name ?? path.basename(source.root)
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
|
|
186
|
+
const previousCdpPort = process.env.CDP_PORT;
|
|
187
|
+
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
188
|
+
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
189
|
+
const previousMetroPort = process.env.METRO_PORT;
|
|
190
|
+
const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
|
|
191
|
+
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
192
|
+
if (runtimeOptions.cdpPort) {
|
|
193
|
+
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
194
|
+
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
195
|
+
}
|
|
196
|
+
if (runtimeOptions.watcherPort) {
|
|
197
|
+
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
198
|
+
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
199
|
+
}
|
|
200
|
+
if (runtimeOptions.launchExistingDist) {
|
|
201
|
+
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
202
|
+
}
|
|
203
|
+
return () => {
|
|
88
204
|
restoreEnv("CDP_PORT", previousCdpPort);
|
|
89
205
|
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
90
206
|
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
91
207
|
restoreEnv("METRO_PORT", previousMetroPort);
|
|
92
|
-
restoreEnv(
|
|
208
|
+
restoreEnv(
|
|
209
|
+
"METAMASK_RECIPE_EXTENSION_AUTOLAUNCH",
|
|
210
|
+
previousExtensionAutolaunch
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function actionSourceMap(sources) {
|
|
215
|
+
const roots = {};
|
|
216
|
+
for (const [action, source] of sources) {
|
|
217
|
+
if (source.tier === "official") continue;
|
|
218
|
+
roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(
|
|
219
|
+
path.dirname(path.dirname(source.manifestPath)),
|
|
220
|
+
"actions"
|
|
221
|
+
));
|
|
93
222
|
}
|
|
223
|
+
return JSON.stringify(roots);
|
|
94
224
|
}
|
|
95
225
|
function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
96
226
|
const base = {
|
|
@@ -113,7 +243,8 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
|
113
243
|
};
|
|
114
244
|
}
|
|
115
245
|
async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
|
|
116
|
-
if (adapter !== "extension" || runtimeOptions.skipExtensionRuntimePrepare === true)
|
|
246
|
+
if (adapter !== "extension" || runtimeOptions.skipExtensionRuntimePrepare === true)
|
|
247
|
+
return;
|
|
117
248
|
const { prepareExtensionRuntime } = await import("../adapters/extension/runtime.js");
|
|
118
249
|
await prepareExtensionRuntime({
|
|
119
250
|
projectRoot,
|
|
@@ -128,21 +259,74 @@ function restoreEnv(key, value) {
|
|
|
128
259
|
if (value === void 0) delete process.env[key];
|
|
129
260
|
else process.env[key] = value;
|
|
130
261
|
}
|
|
131
|
-
async function validateRecipeAdapterAware(recipe, manifest, librarySources) {
|
|
132
|
-
const
|
|
133
|
-
let
|
|
262
|
+
async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySources, rootRef, params) {
|
|
263
|
+
const protocol = await importRecipeProtocol();
|
|
264
|
+
let externalRecipeIds;
|
|
265
|
+
let libraryResolution;
|
|
266
|
+
const harness = await importRecipeHarness();
|
|
134
267
|
if (librarySources && librarySources.length > 0) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
const withManifest = validateRecipeWithManifest(
|
|
142
|
-
|
|
143
|
-
|
|
268
|
+
libraryResolution = await harness.loadRecipeLibraries(librarySources, {
|
|
269
|
+
adapter
|
|
270
|
+
});
|
|
271
|
+
externalRecipeIds = new Set(libraryResolution.recipes.keys());
|
|
272
|
+
}
|
|
273
|
+
const validationOptions = externalRecipeIds !== void 0 ? { externalRecipeIds } : void 0;
|
|
274
|
+
const withManifest = protocol.validateRecipeWithManifest(
|
|
275
|
+
recipe,
|
|
276
|
+
manifest,
|
|
277
|
+
validationOptions
|
|
278
|
+
);
|
|
279
|
+
const findings = [...withManifest.findings];
|
|
280
|
+
if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
|
|
281
|
+
try {
|
|
282
|
+
const digest = protocol.digestRecipeDocument(recipe);
|
|
283
|
+
const dependencies = harness.resolveRecipeDependencies({
|
|
284
|
+
rootRef: rootRef ?? `$root:${digest}`,
|
|
285
|
+
root: recipe,
|
|
286
|
+
rootSource: {
|
|
287
|
+
kind: "recipe-file",
|
|
288
|
+
trust: "unknown",
|
|
289
|
+
name: rootRef ?? "recipe file",
|
|
290
|
+
digest
|
|
291
|
+
},
|
|
292
|
+
recipes: libraryResolution.recipes
|
|
293
|
+
});
|
|
294
|
+
if (params) {
|
|
295
|
+
harness.validateRecipeDependencyParams({
|
|
296
|
+
root: recipe,
|
|
297
|
+
params,
|
|
298
|
+
recipes: dependencies.recipes
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
for (const dependency of dependencies.recipes.values()) {
|
|
302
|
+
findings.push(
|
|
303
|
+
...protocol.validateRecipeWithManifest(
|
|
304
|
+
dependency.document,
|
|
305
|
+
manifest,
|
|
306
|
+
validationOptions
|
|
307
|
+
).findings
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const resolutionError = error instanceof harness.RecipeResolutionError ? error : void 0;
|
|
312
|
+
if (!resolutionError) throw error;
|
|
313
|
+
findings.push({
|
|
314
|
+
severity: "error",
|
|
315
|
+
code: resolutionError.code,
|
|
316
|
+
path: "workflow",
|
|
317
|
+
message: `${resolutionError.message} Next: ${resolutionError.userAction}`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const errors = findings.filter(
|
|
322
|
+
(finding) => finding.severity === "error"
|
|
323
|
+
).length;
|
|
144
324
|
const warnings = findings.length - errors;
|
|
145
|
-
return {
|
|
325
|
+
return {
|
|
326
|
+
status: errors > 0 ? "invalid" : "valid",
|
|
327
|
+
findings,
|
|
328
|
+
summary: { errors, warnings }
|
|
329
|
+
};
|
|
146
330
|
}
|
|
147
331
|
function isRecipeFile(p) {
|
|
148
332
|
try {
|
|
@@ -151,107 +335,169 @@ function isRecipeFile(p) {
|
|
|
151
335
|
return false;
|
|
152
336
|
}
|
|
153
337
|
}
|
|
154
|
-
function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
338
|
+
async function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
155
339
|
const direct = path.resolve(recipeArg);
|
|
156
340
|
if (isRecipeFile(direct)) return { recipeFile: direct };
|
|
341
|
+
const sources = effectiveLibrarySources(librarySources);
|
|
342
|
+
const harness = await importRecipeHarness();
|
|
343
|
+
const resolution = await harness.loadRecipeLibraries(sources, { adapter });
|
|
157
344
|
if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
for (const source of librarySources) {
|
|
161
|
-
for (const candidate of candidates) {
|
|
162
|
-
const file = path.join(source.root, "recipes", candidate);
|
|
163
|
-
if (isRecipeFile(file)) return { recipeFile: file };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
for (const candidate of candidates) {
|
|
168
|
-
const file = recipePath(candidate);
|
|
169
|
-
if (isRecipeFile(file)) return { recipeFile: file };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
345
|
+
const selected = resolution.recipes.get(recipeArg);
|
|
346
|
+
if (selected) return { recipeFile: selected.path, ref: selected.ref };
|
|
172
347
|
}
|
|
173
|
-
const
|
|
174
|
-
const nonCanonical =
|
|
175
|
-
const notFoundCore = nonCanonical.length > 0 ? `recipe not found: ${recipeArg} \u2014 not a file, and no recipe matched in library sources [${
|
|
176
|
-
const suffix =
|
|
348
|
+
const available = [...resolution.recipes.keys()].sort();
|
|
349
|
+
const nonCanonical = sources.filter((source) => source.name !== "metamask");
|
|
350
|
+
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.`;
|
|
351
|
+
const suffix = available.length > 0 ? ` Library recipes for ${adapter}: ${available.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
|
|
177
352
|
return { notFound: notFoundCore + suffix };
|
|
178
353
|
}
|
|
179
|
-
function
|
|
180
|
-
|
|
354
|
+
function effectiveLibrarySources(librarySources) {
|
|
355
|
+
return librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: path.resolve(runnerDir, "library") }];
|
|
356
|
+
}
|
|
357
|
+
async function listRunnableRecipes(adapter, librarySources) {
|
|
358
|
+
const harness = await importRecipeHarness();
|
|
359
|
+
const resolution = await harness.loadRecipeLibraries(
|
|
360
|
+
effectiveLibrarySources(librarySources),
|
|
361
|
+
{
|
|
362
|
+
adapter
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
return [...resolution.recipes.values()].map(
|
|
366
|
+
(recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
|
|
367
|
+
).sort((left, right) => left.name.localeCompare(right.name));
|
|
368
|
+
}
|
|
369
|
+
async function describeRunnableRecipe(recipeArg, adapter, librarySources) {
|
|
370
|
+
const resolved = await resolveRunRecipeArg(
|
|
371
|
+
recipeArg,
|
|
372
|
+
adapter,
|
|
373
|
+
librarySources
|
|
374
|
+
);
|
|
375
|
+
if ("notFound" in resolved) return resolved;
|
|
376
|
+
let document;
|
|
181
377
|
try {
|
|
182
|
-
|
|
183
|
-
} catch {
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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 {
|
|
386
|
+
unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.`
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const harness = await importRecipeHarness();
|
|
390
|
+
const libraryResolution = await harness.loadRecipeLibraries(
|
|
391
|
+
effectiveLibrarySources(librarySources),
|
|
392
|
+
{ adapter }
|
|
393
|
+
);
|
|
394
|
+
const selected = resolved.ref ? libraryResolution.recipes.get(resolved.ref) : void 0;
|
|
395
|
+
const composition = recipeComposition(document, libraryResolution.recipes);
|
|
396
|
+
const summary = recipeSummary(
|
|
397
|
+
resolved.ref ?? recipeArg,
|
|
398
|
+
adapter,
|
|
399
|
+
document,
|
|
400
|
+
selected
|
|
401
|
+
);
|
|
402
|
+
return {
|
|
403
|
+
recipe: {
|
|
404
|
+
...summary,
|
|
405
|
+
source: selected?.source ?? "file",
|
|
406
|
+
file: selected?.file ?? resolved.recipeFile,
|
|
407
|
+
path: resolved.recipeFile,
|
|
408
|
+
...typeof document.$schema === "string" ? { $schema: document.$schema } : {},
|
|
409
|
+
actions: composition.actions,
|
|
410
|
+
nestedRecipes: composition.nestedRecipes,
|
|
411
|
+
unresolvedRecipes: composition.unresolvedRecipes
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function recipeSummary(ref, adapter, document, selected) {
|
|
416
|
+
return {
|
|
417
|
+
name: ref,
|
|
418
|
+
source: selected?.source ?? "file",
|
|
419
|
+
file: selected?.file ?? "",
|
|
420
|
+
shadows: selected?.shadows ?? [],
|
|
421
|
+
adapter,
|
|
422
|
+
variant: selected?.adapter ?? null,
|
|
423
|
+
parameters: recipeParameters(document),
|
|
424
|
+
...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
|
|
425
|
+
...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function recipeParameters(document) {
|
|
429
|
+
if (!isRecord(document.paramsSchema)) return [];
|
|
430
|
+
const schema = document.paramsSchema;
|
|
431
|
+
const required = new Set(
|
|
432
|
+
Array.isArray(schema.required) ? schema.required.filter(
|
|
433
|
+
(entry) => typeof entry === "string"
|
|
434
|
+
) : []
|
|
435
|
+
);
|
|
436
|
+
if (!isRecord(schema.properties)) return [];
|
|
437
|
+
return Object.entries(schema.properties).map(([name, raw]) => {
|
|
438
|
+
const property = isRecord(raw) ? raw : {};
|
|
439
|
+
return {
|
|
440
|
+
name,
|
|
441
|
+
...typeof property.type === "string" ? { type: property.type } : {},
|
|
442
|
+
required: required.has(name),
|
|
443
|
+
...Object.hasOwn(property, "default") ? { default: property.default } : {},
|
|
444
|
+
...typeof property.description === "string" ? { description: property.description } : {},
|
|
445
|
+
...Array.isArray(property.enum) ? { enum: property.enum } : {}
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function recipeComposition(recipe, recipes) {
|
|
450
|
+
const actions = /* @__PURE__ */ new Set();
|
|
451
|
+
const nestedRecipes = /* @__PURE__ */ new Set();
|
|
452
|
+
const unresolvedRecipes = /* @__PURE__ */ new Set();
|
|
453
|
+
const visitedRecipes = /* @__PURE__ */ new Set();
|
|
454
|
+
const visitNode = (value) => {
|
|
455
|
+
if (!isRecord(value) || typeof value.action !== "string") return;
|
|
456
|
+
if (value.action !== "call") {
|
|
457
|
+
actions.add(value.action);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (typeof value.ref !== "string") return;
|
|
461
|
+
nestedRecipes.add(value.ref);
|
|
462
|
+
const dependency = recipes.get(value.ref);
|
|
463
|
+
if (!dependency) {
|
|
464
|
+
unresolvedRecipes.add(value.ref);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (visitedRecipes.has(value.ref)) return;
|
|
468
|
+
visitedRecipes.add(value.ref);
|
|
469
|
+
visitRecipe(dependency.document);
|
|
470
|
+
};
|
|
471
|
+
const visitWorkflow = (value) => {
|
|
472
|
+
if (!isRecord(value)) return;
|
|
473
|
+
if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
|
|
474
|
+
};
|
|
475
|
+
const visitRecipe = (document) => {
|
|
476
|
+
visitWorkflow(document.workflow);
|
|
477
|
+
};
|
|
478
|
+
visitRecipe(recipe);
|
|
479
|
+
return {
|
|
480
|
+
actions: [...actions].sort(),
|
|
481
|
+
nestedRecipes: [...nestedRecipes].sort(),
|
|
482
|
+
unresolvedRecipes: [...unresolvedRecipes].sort()
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async function validateRunRecipeStatic(recipeArg, adapter, options, params = {}) {
|
|
486
|
+
let librarySources = await resolveMetaMaskLibrarySources(
|
|
487
|
+
optionStrings(options, "library")
|
|
488
|
+
);
|
|
489
|
+
const resolved = await resolveRunRecipeArg(
|
|
490
|
+
recipeArg,
|
|
491
|
+
adapter,
|
|
492
|
+
librarySources
|
|
493
|
+
);
|
|
254
494
|
const recipeFile = "recipeFile" in resolved ? resolved.recipeFile : path.resolve(recipeArg);
|
|
495
|
+
if ("recipeFile" in resolved && !resolved.ref) {
|
|
496
|
+
librarySources = await resolveMetaMaskLibrarySources(
|
|
497
|
+
optionStrings(options, "library"),
|
|
498
|
+
recipeFile
|
|
499
|
+
);
|
|
500
|
+
}
|
|
255
501
|
const empty = {
|
|
256
502
|
recipe: void 0,
|
|
257
503
|
recipeFile,
|
|
@@ -259,6 +505,7 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
259
505
|
errorCount: 0,
|
|
260
506
|
manifestOk: false,
|
|
261
507
|
schemaValid: false,
|
|
508
|
+
effectiveParams: params,
|
|
262
509
|
...librarySources ? { librarySources } : {}
|
|
263
510
|
};
|
|
264
511
|
if ("notFound" in resolved) {
|
|
@@ -283,7 +530,11 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
283
530
|
};
|
|
284
531
|
}
|
|
285
532
|
const findings = [];
|
|
286
|
-
const manifest =
|
|
533
|
+
const { manifest } = await resolveActionManifest(
|
|
534
|
+
adapter,
|
|
535
|
+
optionString(options, "actionManifest"),
|
|
536
|
+
librarySources
|
|
537
|
+
);
|
|
287
538
|
let manifestOk = true;
|
|
288
539
|
try {
|
|
289
540
|
await validateManifest(manifest);
|
|
@@ -296,30 +547,47 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
296
547
|
message: error instanceof Error ? error.message : String(error)
|
|
297
548
|
});
|
|
298
549
|
}
|
|
299
|
-
|
|
300
|
-
findings.push(...validation.findings);
|
|
550
|
+
let effectiveParams = params;
|
|
301
551
|
if (isRecord(recipe)) {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
552
|
+
const protocol = await importRecipeProtocol();
|
|
553
|
+
effectiveParams = protocol.applyRecipeParamDefaults(
|
|
554
|
+
params,
|
|
555
|
+
recipe.paramsSchema
|
|
556
|
+
);
|
|
557
|
+
findings.push(
|
|
558
|
+
...protocol.validateRecipeParams(effectiveParams, recipe.paramsSchema).findings
|
|
559
|
+
);
|
|
560
|
+
const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
|
|
561
|
+
const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
|
|
562
|
+
if (adapter === "extension" && nodes) {
|
|
563
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
564
|
+
if (isRecord(node) && node.action === "metamask.perps.start_state" && node.page === false) {
|
|
565
|
+
findings.push({
|
|
566
|
+
severity: "error",
|
|
567
|
+
code: "recipe.extension-perps-start-page",
|
|
568
|
+
path: `workflow.nodes.${nodeId}.page`,
|
|
569
|
+
message: "Extension metamask.perps.start_state must enter Perps; omit page or provide a string target instead of false"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
320
573
|
}
|
|
321
574
|
}
|
|
322
|
-
const
|
|
575
|
+
const validation = manifestOk ? await validateRecipeAdapterAware(
|
|
576
|
+
adapter,
|
|
577
|
+
recipe,
|
|
578
|
+
manifest,
|
|
579
|
+
librarySources,
|
|
580
|
+
resolved.ref,
|
|
581
|
+
effectiveParams
|
|
582
|
+
) : {
|
|
583
|
+
status: "invalid",
|
|
584
|
+
findings: [],
|
|
585
|
+
summary: { errors: 1, warnings: 0 }
|
|
586
|
+
};
|
|
587
|
+
findings.push(...validation.findings);
|
|
588
|
+
const errorCount = findings.filter(
|
|
589
|
+
(finding) => finding.severity === "error"
|
|
590
|
+
).length;
|
|
323
591
|
return {
|
|
324
592
|
recipe,
|
|
325
593
|
recipeFile,
|
|
@@ -327,13 +595,21 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
327
595
|
errorCount,
|
|
328
596
|
manifestOk,
|
|
329
597
|
schemaValid: validation.status === "valid" && errorCount === 0,
|
|
598
|
+
effectiveParams,
|
|
330
599
|
...librarySources ? { librarySources } : {}
|
|
331
600
|
};
|
|
332
601
|
}
|
|
333
602
|
async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
334
603
|
if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
|
|
335
604
|
try {
|
|
336
|
-
const
|
|
605
|
+
const librarySources = await resolveMetaMaskLibrarySources(
|
|
606
|
+
optionStrings(options, "library")
|
|
607
|
+
);
|
|
608
|
+
const { manifest } = await resolveActionManifest(
|
|
609
|
+
adapter,
|
|
610
|
+
optionString(options, "actionManifest"),
|
|
611
|
+
librarySources
|
|
612
|
+
);
|
|
337
613
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
338
614
|
const actions = getRecipeActionManifestActionNames(manifest);
|
|
339
615
|
const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
|
|
@@ -341,67 +617,119 @@ async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
|
341
617
|
const device = optionString(options, "device");
|
|
342
618
|
const actionManifest = optionString(options, "actionManifest");
|
|
343
619
|
const parts = ["mm-harness", "call", matches[0]];
|
|
620
|
+
parts.push(
|
|
621
|
+
"--adapter",
|
|
622
|
+
adapter,
|
|
623
|
+
"--target",
|
|
624
|
+
shellQuoteArg(targetPath(options))
|
|
625
|
+
);
|
|
344
626
|
if (device) parts.push("--device", shellQuoteArg(device));
|
|
345
|
-
if (actionManifest)
|
|
627
|
+
if (actionManifest)
|
|
628
|
+
parts.push("--action-manifest", shellQuoteArg(actionManifest));
|
|
346
629
|
return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
|
|
347
630
|
} catch {
|
|
348
631
|
return "";
|
|
349
632
|
}
|
|
350
633
|
}
|
|
351
|
-
async function resolveMetaMaskLibrarySources(libraryEntry) {
|
|
634
|
+
async function resolveMetaMaskLibrarySources(libraryEntry, recipePath) {
|
|
352
635
|
const harness = await importRecipeHarness();
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const sources = await harness.resolveRecipeLibrarySources(
|
|
362
|
-
libraryEntry ? { cliEntries: [libraryEntry] } : void 0
|
|
636
|
+
const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
|
|
637
|
+
const sources = await harness.resolveRecipeLibrarySources({
|
|
638
|
+
...cliEntries.length > 0 ? { cliEntries } : {},
|
|
639
|
+
...recipePath ? { recipePath } : {}
|
|
640
|
+
});
|
|
641
|
+
const canonicalRoot = path.resolve(runnerDir, "library");
|
|
642
|
+
const configuredCanonical = sources.find(
|
|
643
|
+
(source) => source.name === "metamask"
|
|
363
644
|
);
|
|
364
|
-
|
|
365
|
-
|
|
645
|
+
if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
|
|
646
|
+
throw usageError(
|
|
647
|
+
`Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
|
|
651
|
+
sources.push({ name: "metamask", root: canonicalRoot });
|
|
652
|
+
}
|
|
653
|
+
return sources.map((source) => ({
|
|
654
|
+
...source,
|
|
655
|
+
provenance: source.provenance ?? {
|
|
656
|
+
kind: source.name === "metamask" ? "bundled" : "library",
|
|
657
|
+
trust: source.name === "metamask" ? "trusted" : "unknown",
|
|
658
|
+
name: source.name ?? path.basename(source.root)
|
|
659
|
+
}
|
|
660
|
+
}));
|
|
366
661
|
}
|
|
367
662
|
function synthesizeOneNodeRecipe(action, args) {
|
|
368
663
|
return {
|
|
369
|
-
|
|
664
|
+
$schema: "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
370
665
|
title: `mm-harness call ${action}`,
|
|
371
666
|
description: `Ad-hoc single-action execution of ${action} via the real engine path (mm-harness call).`,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
667
|
+
workflow: {
|
|
668
|
+
entry: "call",
|
|
669
|
+
nodes: {
|
|
670
|
+
call: {
|
|
671
|
+
...args,
|
|
672
|
+
action,
|
|
673
|
+
next: "done",
|
|
674
|
+
intent: typeof args.intent === "string" ? args.intent : `Complete the requested ${action} operation`
|
|
675
|
+
},
|
|
676
|
+
done: { action: "end", status: "pass" }
|
|
379
677
|
}
|
|
380
678
|
}
|
|
381
679
|
};
|
|
382
680
|
}
|
|
383
681
|
async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
384
|
-
const
|
|
682
|
+
const librarySources = await resolveMetaMaskLibrarySources(void 0);
|
|
683
|
+
const { manifest } = await resolveActionManifest(
|
|
684
|
+
adapter,
|
|
685
|
+
actionManifest,
|
|
686
|
+
librarySources
|
|
687
|
+
);
|
|
385
688
|
const recipe = synthesizeOneNodeRecipe(action, args);
|
|
386
|
-
const validation = await validateRecipeAdapterAware(
|
|
689
|
+
const validation = await validateRecipeAdapterAware(
|
|
690
|
+
adapter,
|
|
691
|
+
recipe,
|
|
692
|
+
manifest,
|
|
693
|
+
librarySources
|
|
694
|
+
);
|
|
387
695
|
if (validation.status === "invalid") return { status: "fail" };
|
|
388
696
|
const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-fixtures-"));
|
|
389
697
|
const recipeFile = path.join(scratch, "set.recipe.json");
|
|
390
698
|
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
391
699
|
`);
|
|
392
|
-
const result = await runRecipe(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
700
|
+
const result = await runRecipe(
|
|
701
|
+
adapter,
|
|
702
|
+
recipeFile,
|
|
703
|
+
path.join(scratch, "artifacts"),
|
|
704
|
+
target,
|
|
705
|
+
actionManifest,
|
|
706
|
+
{
|
|
707
|
+
librarySources,
|
|
708
|
+
stdoutIsMachineContract: true
|
|
709
|
+
}
|
|
710
|
+
);
|
|
396
711
|
return { status: result.status === "pass" ? "pass" : "fail" };
|
|
397
712
|
}
|
|
398
713
|
async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
|
|
399
714
|
if (recipeRunning(target)) {
|
|
400
715
|
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
716
|
+
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
|
|
401
717
|
if (json) {
|
|
402
|
-
console.log(
|
|
718
|
+
console.log(
|
|
719
|
+
JSON.stringify(
|
|
720
|
+
{
|
|
721
|
+
schemaVersion: 1,
|
|
722
|
+
status: "fail",
|
|
723
|
+
recoverable: false,
|
|
724
|
+
error: { code: "RECIPE_RUNNING", message: msg, userAction }
|
|
725
|
+
},
|
|
726
|
+
null,
|
|
727
|
+
2
|
|
728
|
+
)
|
|
729
|
+
);
|
|
403
730
|
} else {
|
|
404
|
-
console.error(`\u2717 mm-harness: ${msg}
|
|
731
|
+
console.error(`\u2717 mm-harness: ${msg}
|
|
732
|
+
Next: ${userAction}`);
|
|
405
733
|
}
|
|
406
734
|
return EXIT.bounded;
|
|
407
735
|
}
|
|
@@ -414,21 +742,43 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
414
742
|
return EXIT.usage;
|
|
415
743
|
}
|
|
416
744
|
const state = newHealState();
|
|
417
|
-
|
|
745
|
+
prepareOptions.onPhase?.("install");
|
|
746
|
+
const ensured = await ensureOverlay(
|
|
747
|
+
adapter,
|
|
748
|
+
target,
|
|
749
|
+
heal,
|
|
750
|
+
state,
|
|
751
|
+
json
|
|
752
|
+
);
|
|
418
753
|
if (!ensured.ok) {
|
|
754
|
+
const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
|
|
419
755
|
if (json) {
|
|
420
756
|
console.log(
|
|
421
757
|
JSON.stringify(
|
|
422
|
-
{
|
|
758
|
+
{
|
|
759
|
+
schemaVersion: 1,
|
|
760
|
+
status: "fail",
|
|
761
|
+
recoverable: false,
|
|
762
|
+
mutations: state.mutations,
|
|
763
|
+
error: {
|
|
764
|
+
code: "OVERLAY_INSTALL_FAILED",
|
|
765
|
+
message: ensured.error,
|
|
766
|
+
userAction
|
|
767
|
+
}
|
|
768
|
+
},
|
|
423
769
|
null,
|
|
424
770
|
2
|
|
425
771
|
)
|
|
426
772
|
);
|
|
427
773
|
} else {
|
|
428
|
-
console.error(
|
|
774
|
+
console.error(
|
|
775
|
+
`\u2717 overlay auto-ensure failed: ${ensured.error}
|
|
776
|
+
Next: ${userAction}`
|
|
777
|
+
);
|
|
429
778
|
}
|
|
430
779
|
return EXIT.infra;
|
|
431
780
|
}
|
|
781
|
+
prepareOptions.onPhase?.("healthcheck");
|
|
432
782
|
if (adapter === "extension") {
|
|
433
783
|
const previousCdpPort = process.env.CDP_PORT;
|
|
434
784
|
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
@@ -441,7 +791,15 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
441
791
|
}
|
|
442
792
|
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
443
793
|
try {
|
|
444
|
-
const current = await ensureExtensionProofRuntime(
|
|
794
|
+
const current = await ensureExtensionProofRuntime(
|
|
795
|
+
target,
|
|
796
|
+
heal,
|
|
797
|
+
state,
|
|
798
|
+
json,
|
|
799
|
+
{
|
|
800
|
+
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
801
|
+
}
|
|
802
|
+
);
|
|
445
803
|
if (current !== null) return current;
|
|
446
804
|
} finally {
|
|
447
805
|
restoreEnv("CDP_PORT", previousCdpPort);
|
|
@@ -450,7 +808,15 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
450
808
|
}
|
|
451
809
|
}
|
|
452
810
|
if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
|
|
453
|
-
const current = await ensureMobileProofRuntime(
|
|
811
|
+
const current = await ensureMobileProofRuntime(
|
|
812
|
+
target,
|
|
813
|
+
heal,
|
|
814
|
+
state,
|
|
815
|
+
json,
|
|
816
|
+
{
|
|
817
|
+
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
818
|
+
}
|
|
819
|
+
);
|
|
454
820
|
if (current !== null) return current;
|
|
455
821
|
}
|
|
456
822
|
return { state, heal };
|
|
@@ -460,19 +826,45 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
460
826
|
const check = deps.check ?? (() => source.mobileSourceCheck(target));
|
|
461
827
|
const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
|
|
462
828
|
const initial = check();
|
|
829
|
+
if (initial.status === "unavailable") {
|
|
830
|
+
if (json) {
|
|
831
|
+
console.log(
|
|
832
|
+
JSON.stringify(
|
|
833
|
+
{
|
|
834
|
+
schemaVersion: 1,
|
|
835
|
+
status: "fail",
|
|
836
|
+
recoverable: false,
|
|
837
|
+
error: initial.error
|
|
838
|
+
},
|
|
839
|
+
null,
|
|
840
|
+
2
|
|
841
|
+
)
|
|
842
|
+
);
|
|
843
|
+
} else {
|
|
844
|
+
console.error(`\u2717 ${initial.error.message}`);
|
|
845
|
+
console.error(` Next: ${initial.error.userAction}`);
|
|
846
|
+
}
|
|
847
|
+
return EXIT.usage;
|
|
848
|
+
}
|
|
463
849
|
if (initial.status === "current") return null;
|
|
464
850
|
const targetArg = JSON.stringify(path.resolve(target));
|
|
465
851
|
const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
|
|
466
852
|
if (heal === "off") {
|
|
467
853
|
const message = `Mobile loaded source is not current (${initial.status}).`;
|
|
468
854
|
if (json) {
|
|
469
|
-
console.log(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
855
|
+
console.log(
|
|
856
|
+
JSON.stringify(
|
|
857
|
+
{
|
|
858
|
+
schemaVersion: 1,
|
|
859
|
+
status: "fail",
|
|
860
|
+
recoverable: true,
|
|
861
|
+
mutations: state.mutations,
|
|
862
|
+
error: { code: "MOBILE_SOURCE_NOT_LOADED", message, userAction }
|
|
863
|
+
},
|
|
864
|
+
null,
|
|
865
|
+
2
|
|
866
|
+
)
|
|
867
|
+
);
|
|
476
868
|
} else {
|
|
477
869
|
console.error(`\u2717 ${message}`);
|
|
478
870
|
console.error(` Next: ${userAction}`);
|
|
@@ -481,7 +873,11 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
481
873
|
}
|
|
482
874
|
const recoveryCode = "mobile.source_reloaded";
|
|
483
875
|
state.attemptedRecoveries.push(recoveryCode);
|
|
484
|
-
|
|
876
|
+
deps.onRecovery?.(recoveryCode);
|
|
877
|
+
if (!json)
|
|
878
|
+
console.error(
|
|
879
|
+
`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`
|
|
880
|
+
);
|
|
485
881
|
const restart = deps.restart ?? (async () => {
|
|
486
882
|
const result2 = await runOneNode(
|
|
487
883
|
"mobile",
|
|
@@ -490,7 +886,10 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
490
886
|
target,
|
|
491
887
|
void 0
|
|
492
888
|
);
|
|
493
|
-
return {
|
|
889
|
+
return {
|
|
890
|
+
status: result2.status === "pass" ? 0 : EXIT.runtime,
|
|
891
|
+
output: result2.status
|
|
892
|
+
};
|
|
494
893
|
});
|
|
495
894
|
const result = await restart();
|
|
496
895
|
const platform = resolveRunMobilePlatform();
|
|
@@ -504,23 +903,46 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
504
903
|
if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
|
|
505
904
|
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.";
|
|
506
905
|
if (json) {
|
|
507
|
-
console.log(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
906
|
+
console.log(
|
|
907
|
+
JSON.stringify(
|
|
908
|
+
{
|
|
909
|
+
schemaVersion: 1,
|
|
910
|
+
status: "fail",
|
|
911
|
+
recoverable: false,
|
|
912
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
913
|
+
error: {
|
|
914
|
+
code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED",
|
|
915
|
+
message,
|
|
916
|
+
userAction,
|
|
917
|
+
originalError: bridge.output || result.output
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
null,
|
|
921
|
+
2
|
|
922
|
+
)
|
|
923
|
+
);
|
|
514
924
|
} else {
|
|
515
925
|
console.error(`\u2717 ${message}`);
|
|
516
|
-
console.error(
|
|
926
|
+
console.error(
|
|
927
|
+
` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`
|
|
928
|
+
);
|
|
517
929
|
}
|
|
518
930
|
return EXIT.infra;
|
|
519
931
|
}
|
|
520
932
|
state.recovered.push(recoveryCode);
|
|
521
|
-
state.mutations.push({
|
|
933
|
+
state.mutations.push({
|
|
934
|
+
type: "runtime",
|
|
935
|
+
action: "restarted",
|
|
936
|
+
reasonCode: `source-${initial.status}`
|
|
937
|
+
});
|
|
522
938
|
return null;
|
|
523
939
|
}
|
|
940
|
+
function resolveRunMobilePlatform() {
|
|
941
|
+
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
942
|
+
return process.env.PLATFORM;
|
|
943
|
+
}
|
|
944
|
+
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
945
|
+
}
|
|
524
946
|
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
525
947
|
const decide = deps.decide ?? (async () => {
|
|
526
948
|
const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
|
|
@@ -532,7 +954,38 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
532
954
|
});
|
|
533
955
|
const initial = await decide();
|
|
534
956
|
if (initial.decision === "ready") return null;
|
|
535
|
-
if (
|
|
957
|
+
if (initial.reasonCode === "runtime-dist-check-failed") {
|
|
958
|
+
const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
|
|
959
|
+
const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
|
|
960
|
+
if (json) {
|
|
961
|
+
console.log(
|
|
962
|
+
JSON.stringify(
|
|
963
|
+
{
|
|
964
|
+
schemaVersion: 1,
|
|
965
|
+
status: "fail",
|
|
966
|
+
recoverable: false,
|
|
967
|
+
error: {
|
|
968
|
+
code: "EXTENSION_RUNTIME_CHECK_FAILED",
|
|
969
|
+
message,
|
|
970
|
+
userAction: userAction2
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
null,
|
|
974
|
+
2
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
} else {
|
|
978
|
+
console.error(`\u2717 ${message}`);
|
|
979
|
+
console.error(` Next: ${userAction2}`);
|
|
980
|
+
}
|
|
981
|
+
return EXIT.infra;
|
|
982
|
+
}
|
|
983
|
+
if (![
|
|
984
|
+
"dist-stale",
|
|
985
|
+
"dist-missing",
|
|
986
|
+
"runtime-dist-stale",
|
|
987
|
+
"runtime-dist-missing"
|
|
988
|
+
].includes(initial.reasonCode)) {
|
|
536
989
|
return null;
|
|
537
990
|
}
|
|
538
991
|
const tier = initial.decision === "build" ? "build" : "quick";
|
|
@@ -542,13 +995,23 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
542
995
|
if (!canHeal) {
|
|
543
996
|
const message = `Extension proof runtime is not current (${initial.reasonCode}): ${initial.reasons[0] ?? initial.decision}`;
|
|
544
997
|
if (json) {
|
|
545
|
-
console.log(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
998
|
+
console.log(
|
|
999
|
+
JSON.stringify(
|
|
1000
|
+
{
|
|
1001
|
+
schemaVersion: 1,
|
|
1002
|
+
status: "fail",
|
|
1003
|
+
recoverable: true,
|
|
1004
|
+
mutations: state.mutations,
|
|
1005
|
+
error: {
|
|
1006
|
+
code: "EXTENSION_RUNTIME_NOT_CURRENT",
|
|
1007
|
+
message,
|
|
1008
|
+
userAction
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
null,
|
|
1012
|
+
2
|
|
1013
|
+
)
|
|
1014
|
+
);
|
|
552
1015
|
} else {
|
|
553
1016
|
console.error(`\u2717 ${message}`);
|
|
554
1017
|
console.error(` Next: ${userAction}`);
|
|
@@ -556,30 +1019,67 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
556
1019
|
return EXIT.infra;
|
|
557
1020
|
}
|
|
558
1021
|
const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
|
|
1022
|
+
const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
|
|
1023
|
+
if (productConfigBlock) {
|
|
1024
|
+
const message = productConfigBlock.message;
|
|
1025
|
+
const userAction2 = productConfigBlock.userAction;
|
|
1026
|
+
if (json) {
|
|
1027
|
+
console.log(
|
|
1028
|
+
JSON.stringify(
|
|
1029
|
+
{
|
|
1030
|
+
schemaVersion: 1,
|
|
1031
|
+
status: "fail",
|
|
1032
|
+
recoverable: false,
|
|
1033
|
+
mutations: state.mutations,
|
|
1034
|
+
error: {
|
|
1035
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
1036
|
+
message,
|
|
1037
|
+
userAction: userAction2
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
null,
|
|
1041
|
+
2
|
|
1042
|
+
)
|
|
1043
|
+
);
|
|
1044
|
+
} else {
|
|
1045
|
+
console.error(`\u2717 ${message}`);
|
|
1046
|
+
console.error(` Next: ${userAction2}`);
|
|
1047
|
+
}
|
|
1048
|
+
return EXIT.infra;
|
|
1049
|
+
}
|
|
559
1050
|
state.attemptedRecoveries.push(recoveryCode);
|
|
560
|
-
|
|
1051
|
+
deps.onRecovery?.(recoveryCode);
|
|
1052
|
+
if (!json)
|
|
1053
|
+
console.error(
|
|
1054
|
+
`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`
|
|
1055
|
+
);
|
|
561
1056
|
const launch = deps.launch ?? (async (requestedTier) => {
|
|
562
1057
|
const { launchExtension } = await import("./launch/extension.js");
|
|
563
1058
|
return launchExtension(target, requestedTier, false);
|
|
564
1059
|
});
|
|
565
1060
|
const result = await launch(tier);
|
|
566
|
-
|
|
567
|
-
if (result.status === 0 && verified.decision !== "ready") {
|
|
568
|
-
for (let attempt = 0; attempt < 8 && verified.decision !== "ready"; attempt += 1) {
|
|
569
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
570
|
-
verified = await decide();
|
|
571
|
-
}
|
|
572
|
-
}
|
|
1061
|
+
const verified = result.status === 0 ? await decide() : initial;
|
|
573
1062
|
if (result.status !== 0 || verified.decision !== "ready") {
|
|
574
1063
|
const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
|
|
575
1064
|
if (json) {
|
|
576
|
-
console.log(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1065
|
+
console.log(
|
|
1066
|
+
JSON.stringify(
|
|
1067
|
+
{
|
|
1068
|
+
schemaVersion: 1,
|
|
1069
|
+
status: "fail",
|
|
1070
|
+
recoverable: false,
|
|
1071
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
1072
|
+
error: {
|
|
1073
|
+
code: "EXTENSION_RUNTIME_RECOVERY_FAILED",
|
|
1074
|
+
message,
|
|
1075
|
+
userAction,
|
|
1076
|
+
originalError: result.output
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
null,
|
|
1080
|
+
2
|
|
1081
|
+
)
|
|
1082
|
+
);
|
|
583
1083
|
} else {
|
|
584
1084
|
console.error(`\u2717 ${message}`);
|
|
585
1085
|
console.error(` Next: ${userAction}`);
|
|
@@ -587,7 +1087,11 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
587
1087
|
return EXIT.infra;
|
|
588
1088
|
}
|
|
589
1089
|
state.recovered.push(recoveryCode);
|
|
590
|
-
state.mutations.push({
|
|
1090
|
+
state.mutations.push({
|
|
1091
|
+
type: "runtime",
|
|
1092
|
+
action: tier === "build" ? "rebuilt" : "reloaded",
|
|
1093
|
+
reasonCode: initial.reasonCode
|
|
1094
|
+
});
|
|
591
1095
|
return null;
|
|
592
1096
|
}
|
|
593
1097
|
const RUN_RECOVERY_CODE = {
|
|
@@ -603,35 +1107,36 @@ async function recoverRunInfra(adapter, target, json) {
|
|
|
603
1107
|
}
|
|
604
1108
|
if (adapter === "mobile") {
|
|
605
1109
|
const { launchMobile } = await import("./launch/mobile.js");
|
|
606
|
-
const platform =
|
|
1110
|
+
const platform = process.env.PLATFORM === "android" ? "android" : "ios";
|
|
607
1111
|
return launchMobile(target, platform, "quick", json);
|
|
608
1112
|
}
|
|
609
1113
|
return { status: 0, output: "headless run retry" };
|
|
610
1114
|
}
|
|
611
|
-
function resolveRunMobilePlatform() {
|
|
612
|
-
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
613
|
-
return process.env.PLATFORM;
|
|
614
|
-
}
|
|
615
|
-
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
616
|
-
}
|
|
617
1115
|
function readRunFailureText(result) {
|
|
618
1116
|
try {
|
|
619
1117
|
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
620
1118
|
const entries = Array.isArray(trace.entries) ? trace.entries : [];
|
|
621
|
-
return entries.filter(
|
|
1119
|
+
return entries.filter(
|
|
1120
|
+
(entry) => entry && entry.ok === false && typeof entry.error === "string"
|
|
1121
|
+
).map((entry) => entry.error).join("\n").trim();
|
|
622
1122
|
} catch {
|
|
623
1123
|
return "";
|
|
624
1124
|
}
|
|
625
1125
|
}
|
|
626
|
-
async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)) {
|
|
1126
|
+
async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false), onRecovery = () => void 0) {
|
|
627
1127
|
let result = await exec();
|
|
628
1128
|
for (; ; ) {
|
|
629
1129
|
if (result.status === "pass") return { result, violation: null };
|
|
630
|
-
const violation = checkHealBounds(
|
|
1130
|
+
const violation = checkHealBounds(
|
|
1131
|
+
target,
|
|
1132
|
+
readRunFailureText(result),
|
|
1133
|
+
state
|
|
1134
|
+
);
|
|
631
1135
|
if (violation !== null) return { result, violation };
|
|
632
1136
|
if (heal === "off") return { result, violation: null };
|
|
633
1137
|
const recoveryCode = RUN_RECOVERY_CODE[adapter];
|
|
634
1138
|
state.attemptedRecoveries.push(recoveryCode);
|
|
1139
|
+
onRecovery(recoveryCode);
|
|
635
1140
|
await recover();
|
|
636
1141
|
result = await exec();
|
|
637
1142
|
if (result.status === "pass") {
|
|
@@ -641,7 +1146,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
641
1146
|
}
|
|
642
1147
|
}
|
|
643
1148
|
function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
644
|
-
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? '
|
|
1149
|
+
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
645
1150
|
if (json) {
|
|
646
1151
|
console.log(
|
|
647
1152
|
JSON.stringify(
|
|
@@ -661,7 +1166,7 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
|
661
1166
|
code: violation.code,
|
|
662
1167
|
message: violation.message,
|
|
663
1168
|
retryable: false,
|
|
664
|
-
userAction
|
|
1169
|
+
userAction,
|
|
665
1170
|
originalError: violation.originalError ?? null
|
|
666
1171
|
}
|
|
667
1172
|
},
|
|
@@ -670,8 +1175,9 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
|
670
1175
|
)
|
|
671
1176
|
);
|
|
672
1177
|
} else {
|
|
1178
|
+
const message = violation.code === "APP_LOGIC_FAILURE" && violation.originalError ? conciseFailureForHuman(violation.originalError) : violation.message;
|
|
673
1179
|
console.error(
|
|
674
|
-
`\u2717 mm-harness ${command}: ${
|
|
1180
|
+
`\u2717 mm-harness ${command}: ${message}` + (violation.originalError && violation.code !== "APP_LOGIC_FAILURE" ? `
|
|
675
1181
|
--- original failure ---
|
|
676
1182
|
${violation.originalError}` : "") + (userAction ? `
|
|
677
1183
|
Next: ${userAction}` : "")
|
|
@@ -681,17 +1187,19 @@ ${violation.originalError}` : "") + (userAction ? `
|
|
|
681
1187
|
}
|
|
682
1188
|
function countRecipeNodes(recipe) {
|
|
683
1189
|
if (!isRecord(recipe)) return void 0;
|
|
684
|
-
const
|
|
685
|
-
const workflow = validate && isRecord(validate.workflow) ? validate.workflow : void 0;
|
|
1190
|
+
const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
|
|
686
1191
|
const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
|
|
687
1192
|
return nodes ? Object.keys(nodes).length : void 0;
|
|
688
1193
|
}
|
|
689
1194
|
export {
|
|
690
1195
|
countRecipeNodes,
|
|
1196
|
+
describeRunnableRecipe,
|
|
691
1197
|
emitHealViolation,
|
|
692
1198
|
ensureExtensionProofRuntime,
|
|
693
1199
|
ensureMobileProofRuntime,
|
|
694
1200
|
executeWithHealBounds,
|
|
1201
|
+
listRunnableRecipes,
|
|
1202
|
+
preflightRecipe,
|
|
695
1203
|
prepareHeal,
|
|
696
1204
|
recoverRunInfra,
|
|
697
1205
|
resolveMetaMaskLibrarySources,
|