@deeeed/metamask-harness 0.70.1 → 0.71.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 +18 -0
- package/adapters/extension/console-tail.mjs +38 -6
- package/adapters/manifest.json +128 -0
- package/adapters/terminal/cleanup.mjs +68 -0
- package/adapters/terminal/inject.mjs +74 -0
- package/adapters/terminal/launch.mjs +628 -0
- package/adapters/terminal/lib/cdp-client.mjs +84 -0
- package/adapters/terminal/lib/display.mjs +115 -0
- package/adapters/terminal/lib/hud.mjs +148 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
- package/adapters/terminal/lib/origin.mjs +30 -0
- package/adapters/terminal/lib/page-script.mjs +246 -0
- package/adapters/terminal/lib/processes.mjs +187 -0
- package/adapters/terminal/lib/readiness.mjs +274 -0
- package/adapters/terminal/lib/strict-wallet.mjs +138 -0
- package/adapters/terminal/stop.mjs +37 -0
- package/adapters/terminal/verify.mjs +48 -0
- package/adapters/terminal/wallet-host.mjs +703 -0
- package/dist/adapters/console-capture.js +122 -0
- package/dist/adapters/extension/console-capture.js +22 -85
- package/dist/adapters/surface.js +3 -1
- package/dist/adapters/terminal/console-capture.js +46 -0
- package/dist/adapters/terminal/stop.js +19 -0
- package/dist/adapters/terminal/surface.js +104 -0
- package/dist/adapters.js +27 -4
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +12 -4
- package/dist/commands/check.js +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/doctor.js +24 -1
- package/dist/commands/domain.js +1 -1
- package/dist/commands/farmslot-ready.js +2 -0
- package/dist/commands/help.js +2 -2
- package/dist/commands/launch/index.js +97 -2
- package/dist/commands/manifest.js +3 -3
- package/dist/commands/recipe-advice.js +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/run-engine.js +1 -1
- package/dist/commands/shared.js +1 -1
- package/dist/doctor.js +3 -0
- package/dist/harness.js +17 -11
- package/dist/manifest.js +5 -0
- package/dist/mm-harness-cli.js +34 -20
- package/dist/paths.js +2 -2
- package/dist/recipe-security.js +12 -3
- package/dist/review/knowledge.js +1 -1
- package/dist/run-diagnostics.js +150 -33
- package/dist/runner.js +1 -1
- package/dist/runtime-context.js +3 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/library/actions/shared/console-allowlist-add.mjs +137 -0
- package/library/actions/shared/console-collector.mjs +230 -0
- package/library/actions/shared/console-findings.mjs +511 -0
- package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
- package/library/actions/terminal/app/launch.mjs +45 -0
- package/library/actions/terminal/perps/_venue.mjs +242 -0
- package/library/actions/terminal/perps/assert_orders.mjs +54 -0
- package/library/actions/terminal/perps/assert_positions.mjs +40 -0
- package/library/actions/terminal/perps/read_orders.mjs +26 -0
- package/library/actions/terminal/perps/read_positions.mjs +25 -0
- package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
- package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
- package/library/actions/terminal/perps/teardown_state.mjs +104 -0
- package/library/actions/terminal/platform/hud.mjs +71 -0
- package/library/actions/terminal/platform/page.mjs +247 -0
- package/library/actions/terminal/platform/runtime.mjs +200 -0
- package/library/actions/terminal/ui/navigate.mjs +31 -0
- package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
- package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
- package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
- package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
- package/library/manifests/terminal.action-manifest.json +1786 -0
- package/package.json +1 -1
package/dist/run-diagnostics.js
CHANGED
|
@@ -3,8 +3,27 @@ import path from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
|
-
import { runnerDir } from "./paths.js";
|
|
7
|
-
import { ensureExtensionConsoleCapture } from "./adapters/extension/console-capture.js";
|
|
6
|
+
import { recipeRuntimePath, runnerDir } from "./paths.js";
|
|
7
|
+
import { ensureExtensionConsoleCapture, extensionCaptureFiles, extensionCdpPort } from "./adapters/extension/console-capture.js";
|
|
8
|
+
import {
|
|
9
|
+
ensureTerminalConsoleCapture,
|
|
10
|
+
terminalCaptureFiles,
|
|
11
|
+
terminalCdpPort,
|
|
12
|
+
terminalRuntimeFile
|
|
13
|
+
} from "./adapters/terminal/console-capture.js";
|
|
14
|
+
import {
|
|
15
|
+
collectorOwnerMarker,
|
|
16
|
+
collectorStatus,
|
|
17
|
+
verifyCapture
|
|
18
|
+
} from "../library/actions/shared/console-collector.mjs";
|
|
19
|
+
import {
|
|
20
|
+
allowlistMatch,
|
|
21
|
+
classifyConsoleRecord,
|
|
22
|
+
consoleRecords,
|
|
23
|
+
libraryRootsFromEnv,
|
|
24
|
+
loadConsoleAllowlist,
|
|
25
|
+
messageSignature
|
|
26
|
+
} from "../library/actions/shared/console-findings.mjs";
|
|
8
27
|
const MAX_CAPTURE_BYTES = 512 * 1024;
|
|
9
28
|
const MAX_FINDINGS = 20;
|
|
10
29
|
const MAX_PREVIEW_CHARS = 320;
|
|
@@ -26,7 +45,9 @@ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
|
26
45
|
const lines = [`${status} \u2014 ${diagnostics.note}`];
|
|
27
46
|
for (const finding of diagnostics.findings) {
|
|
28
47
|
const count = finding.count > 1 ? ` \xD7${finding.count}` : "";
|
|
29
|
-
|
|
48
|
+
const source = finding.source ? ` [${finding.source}]` : "";
|
|
49
|
+
const allowed = finding.allowlisted ? ` (allowlisted: ${finding.allowlisted.reason})` : "";
|
|
50
|
+
lines.push(`${finding.level.toUpperCase()}${count}${source} \u2014 ${finding.preview}${allowed}`);
|
|
30
51
|
}
|
|
31
52
|
return lines;
|
|
32
53
|
}
|
|
@@ -34,27 +55,64 @@ async function beginRunDiagnostics(adapter, projectRoot) {
|
|
|
34
55
|
const source = getAdapterSurface(adapter).appLogSource(projectRoot);
|
|
35
56
|
if (!source) return null;
|
|
36
57
|
const mobileIssueBuffer = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== "1" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
let capture;
|
|
59
|
+
if (adapter === "extension" || adapter === "terminal") {
|
|
60
|
+
const start = adapter === "extension" ? ensureExtensionConsoleCapture : ensureTerminalConsoleCapture;
|
|
61
|
+
const startError = await start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
62
|
+
const { extensionLog, pageLog, pidFile } = adapter === "extension" ? extensionCaptureFiles(projectRoot) : terminalCaptureFiles(projectRoot);
|
|
63
|
+
const cdpPort = adapter === "extension" ? extensionCdpPort() : terminalCdpPort();
|
|
64
|
+
capture = { adapter, projectRoot, extensionLog, pageLog, pidFile, ...cdpPort ? { cdpPort } : {}, ...startError ? { startError } : {} };
|
|
39
65
|
}
|
|
66
|
+
const walletLogPath = adapter === "terminal" ? terminalRuntimeFile(projectRoot, "wallet-requests.jsonl") : null;
|
|
67
|
+
const walletStat = walletLogPath ? safeStat(walletLogPath) : null;
|
|
40
68
|
const stat = safeStat(source.path);
|
|
41
69
|
return {
|
|
42
70
|
source,
|
|
43
71
|
offset: stat?.size ?? 0,
|
|
44
72
|
...stat ? { inode: stat.ino } : {},
|
|
45
|
-
...mobileIssueBuffer ? { mobileIssueBuffer } : {}
|
|
73
|
+
...mobileIssueBuffer ? { mobileIssueBuffer } : {},
|
|
74
|
+
...capture ? { capture } : {},
|
|
75
|
+
...walletLogPath ? { walletLog: { path: walletLogPath, offset: walletStat?.size ?? 0, ...walletStat ? { inode: walletStat.ino } : {} } } : {}
|
|
46
76
|
};
|
|
47
77
|
}
|
|
78
|
+
async function verifyConsoleCapture(capture) {
|
|
79
|
+
if (capture.startError) return { verified: false, detail: `the console collector did not start: ${capture.startError}` };
|
|
80
|
+
if (!capture.cdpPort) return { verified: false, detail: "no CDP port for the console collector" };
|
|
81
|
+
const status = collectorStatus({
|
|
82
|
+
pidFile: capture.pidFile,
|
|
83
|
+
extensionLog: capture.extensionLog,
|
|
84
|
+
marker: collectorOwnerMarker(path.dirname(capture.pidFile)),
|
|
85
|
+
cdpPort: capture.cdpPort
|
|
86
|
+
});
|
|
87
|
+
if (!status.ok) return { verified: false, detail: status.detail };
|
|
88
|
+
let control;
|
|
89
|
+
if (capture.adapter === "extension") {
|
|
90
|
+
let extensionId = null;
|
|
91
|
+
try {
|
|
92
|
+
extensionId = fs.readFileSync(recipeRuntimePath(capture.projectRoot, "extension.id"), "utf8").trim();
|
|
93
|
+
} catch {
|
|
94
|
+
extensionId = null;
|
|
95
|
+
}
|
|
96
|
+
control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
|
|
97
|
+
} else {
|
|
98
|
+
const browser = readJsonRecord(terminalRuntimeFile(capture.projectRoot, "browser.json"));
|
|
99
|
+
if (typeof browser?.appOrigin !== "string") return { verified: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
|
|
100
|
+
control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
|
|
101
|
+
}
|
|
102
|
+
return { verified: control.ok, detail: control.ok ? `${status.detail}; ${control.detail}` : control.detail };
|
|
103
|
+
}
|
|
48
104
|
function isRunDiagnosticsDocument(value) {
|
|
49
105
|
if (!isRecord(value) || value.schemaVersion !== 1 || value.scope !== "recipe-run-application") return false;
|
|
50
106
|
if (value.status !== "clean" && value.status !== "review" && value.status !== "unavailable") return false;
|
|
51
107
|
return typeof value.note === "string" && Array.isArray(value.findings);
|
|
52
108
|
}
|
|
53
|
-
function finishRunDiagnostics(baseline, result) {
|
|
109
|
+
async function finishRunDiagnostics(baseline, result) {
|
|
54
110
|
if (!baseline) return result;
|
|
55
111
|
try {
|
|
56
112
|
const bufferedIssues = baseline.mobileIssueBuffer ? collectMobileIssueBuffer(baseline.mobileIssueBuffer.projectRoot) : void 0;
|
|
57
|
-
const
|
|
113
|
+
const allowlist = loadConsoleAllowlist(recipeLibraryRoots(result.summaryPath));
|
|
114
|
+
const capture = baseline.capture ? await verifyConsoleCapture(baseline.capture) : void 0;
|
|
115
|
+
const { diagnostics, rawAppLog } = collectRunDiagnosticsCapture(baseline, bufferedIssues, allowlist, capture);
|
|
58
116
|
const artifactsDir = path.dirname(result.summaryPath);
|
|
59
117
|
const diagnosticsPath = path.join(artifactsDir, "diagnostics.json");
|
|
60
118
|
fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}
|
|
@@ -87,16 +145,17 @@ function finishRunDiagnostics(baseline, result) {
|
|
|
87
145
|
};
|
|
88
146
|
}
|
|
89
147
|
}
|
|
90
|
-
function collectRunDiagnostics(baseline, bufferedIssues) {
|
|
91
|
-
return collectRunDiagnosticsCapture(baseline, bufferedIssues).diagnostics;
|
|
148
|
+
function collectRunDiagnostics(baseline, bufferedIssues, allowlist = null, capture) {
|
|
149
|
+
return collectRunDiagnosticsCapture(baseline, bufferedIssues, allowlist, capture).diagnostics;
|
|
92
150
|
}
|
|
93
|
-
function collectRunDiagnosticsCapture(baseline, bufferedIssues) {
|
|
151
|
+
function collectRunDiagnosticsCapture(baseline, bufferedIssues, allowlist = null, capture) {
|
|
94
152
|
const stat = safeStat(baseline.source.path);
|
|
95
153
|
const source = {
|
|
96
154
|
label: baseline.source.label,
|
|
97
155
|
path: baseline.source.path,
|
|
98
156
|
startOffset: baseline.offset,
|
|
99
157
|
endOffset: stat?.size ?? baseline.offset,
|
|
158
|
+
inode: void 0,
|
|
100
159
|
bytesRead: 0,
|
|
101
160
|
truncated: false,
|
|
102
161
|
inAppBuffer: bufferedIssues === void 0 ? "n/a" : bufferedIssues === null ? "unavailable" : "collected",
|
|
@@ -111,22 +170,31 @@ function collectRunDiagnosticsCapture(baseline, bufferedIssues) {
|
|
|
111
170
|
const bytesToRead = Math.min(available, MAX_CAPTURE_BYTES);
|
|
112
171
|
source.startOffset = startOffset;
|
|
113
172
|
source.endOffset = stat.size;
|
|
173
|
+
source.inode = stat.ino;
|
|
114
174
|
source.bytesRead = bytesToRead;
|
|
115
175
|
source.truncated = available > MAX_CAPTURE_BYTES;
|
|
116
176
|
if (bytesToRead > 0) rawAppLog = readSlice(baseline.source.path, startOffset, bytesToRead);
|
|
177
|
+
if (source.truncated) rawAppLog = rawAppLog.subarray(0, rawAppLog.lastIndexOf(10) + 1);
|
|
117
178
|
}
|
|
118
179
|
const text = rawAppLog.toString("utf8");
|
|
180
|
+
const lineFindings = consoleRecords(text).map(classifyRecord).filter((finding) => finding !== null);
|
|
181
|
+
applyConsoleAllowlist(lineFindings, allowlist);
|
|
119
182
|
const allFindings = dedupeFindings(
|
|
120
183
|
[
|
|
121
|
-
...
|
|
122
|
-
...(bufferedIssues ?? []).map(classifyBufferedIssue).filter((finding) => finding !== null)
|
|
184
|
+
...lineFindings,
|
|
185
|
+
...(bufferedIssues ?? []).map(classifyBufferedIssue).filter((finding) => finding !== null),
|
|
186
|
+
...baseline.walletLog ? walletLogFindings(baseline.walletLog) : []
|
|
123
187
|
]
|
|
124
188
|
);
|
|
125
|
-
const
|
|
189
|
+
const applied = allFindings.filter((finding) => finding.level === "info").length;
|
|
190
|
+
allFindings.sort((a, b) => Number(a.level === "info") - Number(b.level === "info"));
|
|
191
|
+
const findings = allFindings.slice(0, MAX_FINDINGS).map(stripInternal);
|
|
126
192
|
const counts = countFindings(allFindings);
|
|
127
193
|
const omittedFindingCount = Math.max(0, allFindings.length - findings.length);
|
|
128
|
-
const
|
|
129
|
-
const
|
|
194
|
+
const captureFailed = capture !== void 0 && !capture.verified;
|
|
195
|
+
const status = counts.total > 0 ? "review" : !captureFailed && !source.truncated && (stat || bufferedIssues !== void 0 && bufferedIssues !== null) ? "clean" : "unavailable";
|
|
196
|
+
const captureNote = (captureFailed ? ` Console capture was not verified: ${capture.detail}.` : "") + (source.truncated ? ` The run's log exceeded ${MAX_CAPTURE_BYTES} bytes; only its start was checked.` : "");
|
|
197
|
+
const note = (counts.total > 0 ? `Observed ${counts.total} distinct application warning/error event(s) during the recipe run; showing ${findings.length} preview(s) and omitting ${omittedFindingCount}. Relation to the task is not determined.` : status === "clean" ? "No application warnings or errors were emitted during the recipe run." : "Application diagnostics were unavailable for this run.") + captureNote;
|
|
130
198
|
return { diagnostics: {
|
|
131
199
|
schemaVersion: 1,
|
|
132
200
|
scope: "recipe-run-application",
|
|
@@ -135,24 +203,47 @@ function collectRunDiagnosticsCapture(baseline, bufferedIssues) {
|
|
|
135
203
|
note,
|
|
136
204
|
source,
|
|
137
205
|
counts,
|
|
206
|
+
...allowlist ? { allowlist: { entries: allowlist.entries.length, applied, problems: allowlist.problems } } : {},
|
|
207
|
+
...capture ? { capture } : {},
|
|
138
208
|
displayedFindingCount: findings.length,
|
|
139
209
|
omittedFindingCount,
|
|
140
210
|
findings
|
|
141
211
|
}, rawAppLog };
|
|
142
212
|
}
|
|
143
|
-
function
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
213
|
+
function classifyRecord(record) {
|
|
214
|
+
const classified = classifyConsoleRecord(record);
|
|
215
|
+
if (!classified) return null;
|
|
216
|
+
const line = record.line;
|
|
217
|
+
const signature = messageSignature(redactDiagnosticText(classified.message));
|
|
218
|
+
return {
|
|
219
|
+
...makeFinding(classified.level, line.trim(), `${classified.source}|${signature}`),
|
|
220
|
+
source: classified.source,
|
|
221
|
+
signature: redactPreview(signature),
|
|
222
|
+
firstFrame: classified.firstFrame ? redactPreview(classified.firstFrame) : null,
|
|
223
|
+
event: classified.key
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function walletLogFindings(walletLog) {
|
|
227
|
+
const stat = safeStat(walletLog.path);
|
|
228
|
+
if (!stat) return [];
|
|
229
|
+
const sameFile = walletLog.inode === void 0 || walletLog.inode === stat.ino;
|
|
230
|
+
const start = sameFile && stat.size >= walletLog.offset ? walletLog.offset : 0;
|
|
231
|
+
const findings = [];
|
|
232
|
+
for (const line of readSlice(walletLog.path, start, stat.size - start).toString("utf8").split("\n")) {
|
|
233
|
+
let entry;
|
|
234
|
+
try {
|
|
235
|
+
entry = JSON.parse(line);
|
|
236
|
+
} catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(entry)) continue;
|
|
240
|
+
let text = null;
|
|
241
|
+
if (entry.kind === "blocked-mainnet" && entry.probe !== true) text = `blocked-mainnet: ${String(entry.transport ?? "request")} to ${String(entry.url ?? "?")}`;
|
|
242
|
+
else if (entry.kind === "refused-mainnet") text = `refused-mainnet: ${String(entry.method ?? "?")} (${String(entry.reason ?? "?")})`;
|
|
243
|
+
else if (entry.kind === "unattributed") text = `unattributed wallet ${entry.binding === "request" ? "request" : "log entry"}${typeof entry.method === "string" ? ` (${entry.method})` : ""}`;
|
|
244
|
+
if (text) findings.push({ ...makeFinding("error", text, `wallet|${text}`), source: "wallet", signature: text, firstFrame: null });
|
|
153
245
|
}
|
|
154
|
-
|
|
155
|
-
return makeFinding(level, trimmed);
|
|
246
|
+
return findings;
|
|
156
247
|
}
|
|
157
248
|
function classifyBufferedIssue(value) {
|
|
158
249
|
if (!isRecord(value) || typeof value.text !== "string") return null;
|
|
@@ -160,15 +251,26 @@ function classifyBufferedIssue(value) {
|
|
|
160
251
|
const level = rawLevel === "warn" || rawLevel === "warning" ? "warning" : rawLevel === "error" ? "error" : rawLevel === "exception" ? "exception" : null;
|
|
161
252
|
return level ? makeFinding(level, value.text) : null;
|
|
162
253
|
}
|
|
163
|
-
function makeFinding(level, text) {
|
|
254
|
+
function makeFinding(level, text, groupKey) {
|
|
164
255
|
const preview = redactPreview(text.trim());
|
|
165
|
-
const identity = preview.replace(/\b\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?\b/gu, "[TIME]").replace(/\[(?:sw|page:[^\]]+|console:[^\]]+)\]/gu, "[APP]").replace(/^(?:(?:WARN(?:ING)?|ERROR|EXCEPTION|\[TIME\]|\[APP\])\s*)+/u, "");
|
|
256
|
+
const identity = groupKey ?? preview.replace(/\b\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?\b/gu, "[TIME]").replace(/\[(?:sw|page:[^\]]+|console:[^\]]+)\]/gu, "[APP]").replace(/^(?:(?:WARN(?:ING)?|ERROR|EXCEPTION|\[TIME\]|\[APP\])\s*)+/u, "");
|
|
166
257
|
return {
|
|
167
258
|
level,
|
|
168
259
|
fingerprint: createHash("sha256").update(`${level}|${identity}`).digest("hex").slice(0, 12),
|
|
169
260
|
preview
|
|
170
261
|
};
|
|
171
262
|
}
|
|
263
|
+
function applyConsoleAllowlist(findings, allowlist) {
|
|
264
|
+
if (!allowlist) return;
|
|
265
|
+
for (const finding of findings) {
|
|
266
|
+
if (!finding.event) continue;
|
|
267
|
+
const match = allowlistMatch({ source: finding.source, key: finding.event }, allowlist);
|
|
268
|
+
if (!match) continue;
|
|
269
|
+
finding.allowlisted = { reason: match.reason, entry: match.index, file: match.file, ...match.temporary ? { temporary: true } : {} };
|
|
270
|
+
finding.level = "info";
|
|
271
|
+
finding.fingerprint = createHash("sha256").update(`${finding.fingerprint}|info|${match.file}#${match.index}`).digest("hex").slice(0, 12);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
172
274
|
function armMobileIssueBuffer(projectRoot) {
|
|
173
275
|
const armed = runMobileIssueCommand(projectRoot, "issues-arm");
|
|
174
276
|
if (!armed) return false;
|
|
@@ -209,10 +311,17 @@ function dedupeFindings(findings) {
|
|
|
209
311
|
return [...byFingerprint.values()];
|
|
210
312
|
}
|
|
211
313
|
function countFindings(findings) {
|
|
212
|
-
const counts = { total:
|
|
213
|
-
for (const finding of findings)
|
|
314
|
+
const counts = { total: 0, warning: 0, error: 0, exception: 0, info: 0 };
|
|
315
|
+
for (const finding of findings) {
|
|
316
|
+
counts[finding.level] += 1;
|
|
317
|
+
if (finding.level !== "info") counts.total += 1;
|
|
318
|
+
}
|
|
214
319
|
return counts;
|
|
215
320
|
}
|
|
321
|
+
function stripInternal(finding) {
|
|
322
|
+
const { event: _event, ...rest } = finding;
|
|
323
|
+
return rest;
|
|
324
|
+
}
|
|
216
325
|
function readSlice(filePath, offset, length) {
|
|
217
326
|
const fd = fs.openSync(filePath, "r");
|
|
218
327
|
try {
|
|
@@ -266,6 +375,13 @@ function indexDiagnosticSummary(summaryPath, diagnostics) {
|
|
|
266
375
|
fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}
|
|
267
376
|
`);
|
|
268
377
|
}
|
|
378
|
+
function recipeLibraryRoots(summaryPath) {
|
|
379
|
+
const summary = readJsonRecord(summaryPath);
|
|
380
|
+
const libraries = isRecord(summary?.recipeLibraries) ? summary.recipeLibraries : null;
|
|
381
|
+
const sources = Array.isArray(libraries?.sources) ? libraries.sources : [];
|
|
382
|
+
const roots = sources.map((source) => isRecord(source) && typeof source.root === "string" ? source.root : null).filter((root) => Boolean(root));
|
|
383
|
+
return roots.length ? roots : libraryRootsFromEnv();
|
|
384
|
+
}
|
|
269
385
|
function readJsonRecord(filePath) {
|
|
270
386
|
try {
|
|
271
387
|
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -282,5 +398,6 @@ export {
|
|
|
282
398
|
collectRunDiagnostics,
|
|
283
399
|
finishRunDiagnostics,
|
|
284
400
|
formatRunDiagnosticsForHuman,
|
|
285
|
-
readRunDiagnosticsDocument
|
|
401
|
+
readRunDiagnosticsDocument,
|
|
402
|
+
verifyConsoleCapture
|
|
286
403
|
};
|
package/dist/runner.js
CHANGED
|
@@ -89,7 +89,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
89
89
|
const { createAgentDeviceUiTransport } = await importMetaMaskNativeUiProvider();
|
|
90
90
|
return createAgentDeviceUiTransport(options2);
|
|
91
91
|
}
|
|
92
|
-
}, preparedLiveAdapters)
|
|
92
|
+
}, preparedLiveAdapters, { hudPolicy: options.autoHud === true ? "show" : "auto" })
|
|
93
93
|
});
|
|
94
94
|
const existing = new Set([...core, ...ui].map((entry) => entry.action));
|
|
95
95
|
const lifecycle = mobileSourceAwareLifecycleAdapters(
|
package/dist/runtime-context.js
CHANGED
|
@@ -91,7 +91,7 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
|
91
91
|
if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "simulatorUdid", "adbSerial"] : ["cdpPort", "extensionId"];
|
|
94
|
+
const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "simulatorUdid", "adbSerial"] : adapter === "terminal" ? ["metroPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"] : ["cdpPort", "extensionId"];
|
|
95
95
|
if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
|
|
96
96
|
return true;
|
|
97
97
|
}
|
|
@@ -107,7 +107,7 @@ function localDefaults(repoRoot, adapter, existing) {
|
|
|
107
107
|
if (adapter === "extension" && !numericEnv("RECIPE_CDP_PORT", "CDP_PORT") && !existing.cdpPort) {
|
|
108
108
|
defaults.cdpPort = claimLocalPort(repoRoot, "cdp", 2e4 + offset);
|
|
109
109
|
}
|
|
110
|
-
if (adapter !== "core" && !numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") && !existing.watcherPort && !existing.metroPort && !existing.devServerPort) {
|
|
110
|
+
if (adapter !== "core" && adapter !== "terminal" && !numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") && !existing.watcherPort && !existing.metroPort && !existing.devServerPort) {
|
|
111
111
|
defaults.watcherPort = claimLocalPort(repoRoot, "watcher", (adapter === "extension" ? 3e4 : 4e4) + offset);
|
|
112
112
|
}
|
|
113
113
|
return defaults;
|
|
@@ -164,7 +164,7 @@ function runtimeResources(adapter, existing, defaults) {
|
|
|
164
164
|
watcherPort,
|
|
165
165
|
devServerPort: watcherPort
|
|
166
166
|
};
|
|
167
|
-
if (adapter === "extension") {
|
|
167
|
+
if (adapter === "extension" || adapter === "terminal") {
|
|
168
168
|
shared.cdpPort = numericEnv("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort;
|
|
169
169
|
} else {
|
|
170
170
|
shared.metroPort = watcherPort;
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -93,7 +93,7 @@ The input path is argv 1 and `METAMASK_RECIPE_ADAPTER_INPUT`. Its document is:
|
|
|
93
93
|
```json
|
|
94
94
|
{
|
|
95
95
|
"schemaVersion": 1,
|
|
96
|
-
"platform": "mobile|extension|core",
|
|
96
|
+
"platform": "mobile|extension|core|terminal",
|
|
97
97
|
"action": "metamask.example.action",
|
|
98
98
|
"node": {},
|
|
99
99
|
"context": {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Write console allowlist entries (schemaVersion 3) from a real capture, so
|
|
3
|
+
// entries are copied from observed events instead of written by hand.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node console-allowlist-add.mjs <run artifacts dir | capture log> [options]
|
|
7
|
+
// --allowlist <file> the library's diagnostics/console-allowlist.json
|
|
8
|
+
// (default: from RECIPE_LIBRARY_PATH, first library)
|
|
9
|
+
// --reason <text> why these events are noise (required with --write)
|
|
10
|
+
// --link <url> code or issue the reason points at
|
|
11
|
+
// --temporary the entry goes once a known fix lands
|
|
12
|
+
// --pick <n,n,...> the candidates to write (numbers from the listing)
|
|
13
|
+
// --write append the picked candidates to the allowlist
|
|
14
|
+
// (needs --pick: every entry is a deliberate choice)
|
|
15
|
+
// A run artifacts dir is read through its diagnostics.json: the run's slice of
|
|
16
|
+
// the raw slot capture (diagnostics-app-log.txt is redacted, so its query
|
|
17
|
+
// strings would not match). Without --write it lists the candidates: every
|
|
18
|
+
// warning, error or exception no entry matches yet, one per distinct event.
|
|
19
|
+
|
|
20
|
+
import { closeSync, fstatSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
ALLOWLIST_RELATIVE_PATH,
|
|
26
|
+
allowlistEntryFor,
|
|
27
|
+
allowlistMatch,
|
|
28
|
+
classifyConsoleRecord,
|
|
29
|
+
consoleRecords,
|
|
30
|
+
libraryRootsFromEnv,
|
|
31
|
+
loadConsoleAllowlist,
|
|
32
|
+
} from './console-findings.mjs';
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const options = { input: null, allowlist: null, reason: null, link: null, temporary: false, pick: null, write: false };
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
if (arg === '--allowlist') options.allowlist = argv[++index];
|
|
39
|
+
else if (arg === '--reason') options.reason = argv[++index];
|
|
40
|
+
else if (arg === '--link') options.link = argv[++index];
|
|
41
|
+
else if (arg === '--temporary') options.temporary = true;
|
|
42
|
+
else if (arg === '--pick') options.pick = String(argv[++index]).split(',').map(Number);
|
|
43
|
+
else if (arg === '--write') options.write = true;
|
|
44
|
+
else if (!options.input && !arg.startsWith('--')) options.input = arg;
|
|
45
|
+
else throw new Error(`unknown argument ${arg}`);
|
|
46
|
+
}
|
|
47
|
+
if (!options.input) throw new Error('usage: console-allowlist-add.mjs <run artifacts dir | capture log> [--allowlist <file>] [--reason <text>] [--link <url>] [--temporary] [--pick <n,...>] [--write]');
|
|
48
|
+
return options;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The capture text a run diagnosed, or a whole log file.
|
|
52
|
+
export function captureText(input) {
|
|
53
|
+
const run = readJsonOr(path.join(input, 'diagnostics.json'), null);
|
|
54
|
+
if (run) {
|
|
55
|
+
const { source } = run;
|
|
56
|
+
const fd = openSync(source.path, 'r');
|
|
57
|
+
try {
|
|
58
|
+
// The same file the run diagnosed: its inode, and long enough.
|
|
59
|
+
const stat = fstatSync(fd);
|
|
60
|
+
if ((source.inode !== undefined && source.inode !== stat.ino) || stat.size < source.endOffset) {
|
|
61
|
+
throw new Error(`${source.path} is not the capture the run diagnosed (it was rotated or replaced since)`);
|
|
62
|
+
}
|
|
63
|
+
return readFileSync(fd).subarray(source.startOffset, source.endOffset).toString('utf8');
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return readFileSync(input, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Distinct events no entry matches yet, as entries without a reason.
|
|
72
|
+
export function allowlistCandidates(text, allowlist = { entries: [] }) {
|
|
73
|
+
const candidates = new Map();
|
|
74
|
+
for (const record of consoleRecords(text)) {
|
|
75
|
+
const finding = classifyConsoleRecord(record);
|
|
76
|
+
if (!finding || allowlistMatch(finding, allowlist)) continue;
|
|
77
|
+
const entry = allowlistEntryFor(finding);
|
|
78
|
+
const id = JSON.stringify(entry);
|
|
79
|
+
const known = candidates.get(id);
|
|
80
|
+
if (known) known.count += 1;
|
|
81
|
+
else candidates.set(id, { entry, level: finding.level, count: 1 });
|
|
82
|
+
}
|
|
83
|
+
return [...candidates.values()];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readJsonOr(file, fallback) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return fallback;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function appendEntries(file, entries) {
|
|
96
|
+
const document = readJsonOr(file, { schemaVersion: 3, entries: [] });
|
|
97
|
+
if (document.schemaVersion !== 3) throw new Error(`${file} is schemaVersion ${document.schemaVersion}; convert it to 3 first`);
|
|
98
|
+
document.entries.push(...entries);
|
|
99
|
+
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function main(argv) {
|
|
103
|
+
const options = parseArgs(argv);
|
|
104
|
+
const roots = options.allowlist ? [] : libraryRootsFromEnv();
|
|
105
|
+
const file = options.allowlist ?? (roots[0] ? path.join(roots[0], ALLOWLIST_RELATIVE_PATH) : null);
|
|
106
|
+
const allowlist = file ? loadConsoleAllowlist([path.dirname(path.dirname(file))]) : { entries: [], problems: [] };
|
|
107
|
+
const candidates = allowlistCandidates(captureText(options.input), allowlist);
|
|
108
|
+
const picked = options.pick ? candidates.filter((_, index) => options.pick.includes(index + 1)) : candidates;
|
|
109
|
+
if (!options.write) {
|
|
110
|
+
candidates.forEach((candidate, index) => {
|
|
111
|
+
process.stdout.write(`${index + 1}. ${candidate.level} ×${candidate.count}\n${JSON.stringify(candidate.entry, null, 2)}\n`);
|
|
112
|
+
});
|
|
113
|
+
if (candidates.length === 0) process.stdout.write('every warning and error in this capture is already allowlisted\n');
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
if (!file) throw new Error('--write needs --allowlist <file> (or RECIPE_LIBRARY_PATH)');
|
|
117
|
+
if (!options.reason?.trim()) throw new Error('--write needs --reason');
|
|
118
|
+
if (!options.pick?.length) throw new Error('--write needs --pick <n,...>: choose the candidates to allowlist from the listing');
|
|
119
|
+
const entries = picked.map(({ entry }) => ({
|
|
120
|
+
...entry,
|
|
121
|
+
reason: options.reason.trim(),
|
|
122
|
+
...(options.link ? { link: options.link } : {}),
|
|
123
|
+
...(options.temporary ? { temporary: true } : {}),
|
|
124
|
+
}));
|
|
125
|
+
appendEntries(file, entries);
|
|
126
|
+
process.stdout.write(`appended ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} to ${file}\n`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
131
|
+
try {
|
|
132
|
+
process.exitCode = main(process.argv.slice(2));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
process.stderr.write(`console-allowlist-add: ${error.message}\n`);
|
|
135
|
+
process.exitCode = 2;
|
|
136
|
+
}
|
|
137
|
+
}
|