@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/adapters/extension/console-tail.mjs +38 -6
  3. package/adapters/manifest.json +128 -0
  4. package/adapters/terminal/cleanup.mjs +68 -0
  5. package/adapters/terminal/inject.mjs +74 -0
  6. package/adapters/terminal/launch.mjs +628 -0
  7. package/adapters/terminal/lib/cdp-client.mjs +84 -0
  8. package/adapters/terminal/lib/display.mjs +115 -0
  9. package/adapters/terminal/lib/hud.mjs +148 -0
  10. package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
  11. package/adapters/terminal/lib/origin.mjs +30 -0
  12. package/adapters/terminal/lib/page-script.mjs +246 -0
  13. package/adapters/terminal/lib/processes.mjs +187 -0
  14. package/adapters/terminal/lib/readiness.mjs +274 -0
  15. package/adapters/terminal/lib/strict-wallet.mjs +138 -0
  16. package/adapters/terminal/stop.mjs +37 -0
  17. package/adapters/terminal/verify.mjs +48 -0
  18. package/adapters/terminal/wallet-host.mjs +703 -0
  19. package/dist/adapters/console-capture.js +122 -0
  20. package/dist/adapters/extension/console-capture.js +22 -85
  21. package/dist/adapters/surface.js +3 -1
  22. package/dist/adapters/terminal/console-capture.js +46 -0
  23. package/dist/adapters/terminal/stop.js +19 -0
  24. package/dist/adapters/terminal/surface.js +104 -0
  25. package/dist/adapters.js +27 -4
  26. package/dist/cli.js +2 -2
  27. package/dist/command-contract.js +12 -4
  28. package/dist/commands/check.js +1 -1
  29. package/dist/commands/config.js +1 -1
  30. package/dist/commands/doctor.js +24 -1
  31. package/dist/commands/domain.js +1 -1
  32. package/dist/commands/farmslot-ready.js +2 -0
  33. package/dist/commands/help.js +2 -2
  34. package/dist/commands/launch/index.js +97 -2
  35. package/dist/commands/manifest.js +3 -3
  36. package/dist/commands/recipe-advice.js +1 -1
  37. package/dist/commands/review.js +1 -1
  38. package/dist/commands/run-engine.js +1 -1
  39. package/dist/commands/shared.js +1 -1
  40. package/dist/doctor.js +3 -0
  41. package/dist/harness.js +17 -11
  42. package/dist/manifest.js +5 -0
  43. package/dist/mm-harness-cli.js +34 -20
  44. package/dist/paths.js +2 -2
  45. package/dist/recipe-security.js +12 -3
  46. package/dist/review/knowledge.js +1 -1
  47. package/dist/run-diagnostics.js +150 -33
  48. package/dist/runner.js +1 -1
  49. package/dist/runtime-context.js +3 -3
  50. package/docs/CONTRIBUTING.md +1 -1
  51. package/library/actions/shared/console-allowlist-add.mjs +137 -0
  52. package/library/actions/shared/console-collector.mjs +230 -0
  53. package/library/actions/shared/console-findings.mjs +511 -0
  54. package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
  55. package/library/actions/terminal/app/launch.mjs +45 -0
  56. package/library/actions/terminal/perps/_venue.mjs +242 -0
  57. package/library/actions/terminal/perps/assert_orders.mjs +54 -0
  58. package/library/actions/terminal/perps/assert_positions.mjs +40 -0
  59. package/library/actions/terminal/perps/read_orders.mjs +26 -0
  60. package/library/actions/terminal/perps/read_positions.mjs +25 -0
  61. package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
  62. package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
  63. package/library/actions/terminal/perps/teardown_state.mjs +104 -0
  64. package/library/actions/terminal/platform/hud.mjs +71 -0
  65. package/library/actions/terminal/platform/page.mjs +247 -0
  66. package/library/actions/terminal/platform/runtime.mjs +200 -0
  67. package/library/actions/terminal/ui/navigate.mjs +31 -0
  68. package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
  69. package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
  70. package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
  71. package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
  72. package/library/manifests/terminal.action-manifest.json +1786 -0
  73. package/package.json +1 -1
@@ -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
- lines.push(`${finding.level.toUpperCase()}${count} \u2014 ${finding.preview}`);
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
- if (adapter === "extension") {
38
- await ensureExtensionConsoleCapture(projectRoot).catch(() => void 0);
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 { diagnostics, rawAppLog } = collectRunDiagnosticsCapture(baseline, bufferedIssues);
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
- ...text.split(/\r?\n/u).map(classifyLine).filter((finding) => finding !== null),
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 findings = allFindings.slice(0, MAX_FINDINGS);
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 status = counts.total > 0 ? "review" : stat || bufferedIssues !== void 0 && bufferedIssues !== null ? "clean" : "unavailable";
129
- 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.";
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 classifyLine(line) {
144
- const trimmed = line.trim();
145
- if (!trimmed) return null;
146
- let level = null;
147
- if (/\bEXCEPTION\s{2,}|\b(?:Uncaught|UnhandledPromiseRejection|FATAL EXCEPTION)\b/iu.test(trimmed)) {
148
- level = "exception";
149
- } else if (/^(?:ERROR\s{2,}|\[error\]\s*)/iu.test(trimmed) || /\]\s+ERROR\s{2,}/u.test(trimmed)) {
150
- level = "error";
151
- } else if (/^(?:WARN(?:ING)?\s{2,}|\[warn(?:ing)?\]\s*)/iu.test(trimmed) || /\]\s+WARN(?:ING)?\s{2,}/u.test(trimmed)) {
152
- level = "warning";
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
- if (!level) return null;
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: findings.length, warning: 0, error: 0, exception: 0 };
213
- for (const finding of findings) counts[finding.level] += 1;
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(
@@ -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;
@@ -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
+ }