@deeeed/metamask-harness 0.70.0 → 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 (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +16 -9
  3. package/adapters/extension/console-tail.mjs +38 -6
  4. package/adapters/extension/launch-browser.cjs +16 -4
  5. package/adapters/manifest.json +128 -0
  6. package/adapters/shared/browser-cdp.cjs +21 -7
  7. package/adapters/shared/browser-resolver.cjs +22 -11
  8. package/adapters/terminal/cleanup.mjs +68 -0
  9. package/adapters/terminal/inject.mjs +74 -0
  10. package/adapters/terminal/launch.mjs +628 -0
  11. package/adapters/terminal/lib/cdp-client.mjs +84 -0
  12. package/adapters/terminal/lib/display.mjs +115 -0
  13. package/adapters/terminal/lib/hud.mjs +148 -0
  14. package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
  15. package/adapters/terminal/lib/origin.mjs +30 -0
  16. package/adapters/terminal/lib/page-script.mjs +246 -0
  17. package/adapters/terminal/lib/processes.mjs +187 -0
  18. package/adapters/terminal/lib/readiness.mjs +274 -0
  19. package/adapters/terminal/lib/strict-wallet.mjs +138 -0
  20. package/adapters/terminal/stop.mjs +37 -0
  21. package/adapters/terminal/verify.mjs +48 -0
  22. package/adapters/terminal/wallet-host.mjs +703 -0
  23. package/dist/adapters/console-capture.js +122 -0
  24. package/dist/adapters/extension/console-capture.js +22 -85
  25. package/dist/adapters/surface.js +3 -1
  26. package/dist/adapters/terminal/console-capture.js +46 -0
  27. package/dist/adapters/terminal/stop.js +19 -0
  28. package/dist/adapters/terminal/surface.js +104 -0
  29. package/dist/adapters.js +27 -4
  30. package/dist/cli.js +2 -2
  31. package/dist/command-contract.js +12 -4
  32. package/dist/commands/check.js +1 -1
  33. package/dist/commands/config.js +1 -1
  34. package/dist/commands/doctor.js +24 -1
  35. package/dist/commands/domain.js +1 -1
  36. package/dist/commands/farmslot-ready.js +2 -0
  37. package/dist/commands/help.js +2 -2
  38. package/dist/commands/launch/index.js +97 -2
  39. package/dist/commands/manifest.js +3 -3
  40. package/dist/commands/recipe-advice.js +1 -1
  41. package/dist/commands/review.js +1 -1
  42. package/dist/commands/run-engine.js +1 -1
  43. package/dist/commands/shared.js +1 -1
  44. package/dist/doctor.js +4 -1
  45. package/dist/harness.js +17 -11
  46. package/dist/manifest.js +5 -0
  47. package/dist/mm-harness-cli.js +34 -20
  48. package/dist/paths.js +2 -2
  49. package/dist/recipe-security.js +12 -3
  50. package/dist/review/knowledge.js +1 -1
  51. package/dist/run-diagnostics.js +150 -33
  52. package/dist/runner.js +1 -1
  53. package/dist/runtime-context.js +3 -3
  54. package/docs/CONTRIBUTING.md +1 -1
  55. package/library/actions/extension/platform/cdp.mjs +35 -10
  56. package/library/actions/shared/console-allowlist-add.mjs +137 -0
  57. package/library/actions/shared/console-collector.mjs +230 -0
  58. package/library/actions/shared/console-findings.mjs +511 -0
  59. package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
  60. package/library/actions/terminal/app/launch.mjs +45 -0
  61. package/library/actions/terminal/perps/_venue.mjs +242 -0
  62. package/library/actions/terminal/perps/assert_orders.mjs +54 -0
  63. package/library/actions/terminal/perps/assert_positions.mjs +40 -0
  64. package/library/actions/terminal/perps/read_orders.mjs +26 -0
  65. package/library/actions/terminal/perps/read_positions.mjs +25 -0
  66. package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
  67. package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
  68. package/library/actions/terminal/perps/teardown_state.mjs +104 -0
  69. package/library/actions/terminal/platform/hud.mjs +71 -0
  70. package/library/actions/terminal/platform/page.mjs +247 -0
  71. package/library/actions/terminal/platform/runtime.mjs +200 -0
  72. package/library/actions/terminal/ui/navigate.mjs +31 -0
  73. package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
  74. package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
  75. package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
  76. package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
  77. package/library/manifests/terminal.action-manifest.json +1786 -0
  78. package/package.json +1 -1
@@ -0,0 +1,230 @@
1
+ // The console collector (adapters/extension/console-tail.mjs) behind run
2
+ // diagnostics and assert_no_console_errors: how a collector is recognised as
3
+ // this runtime's own, and the evidence that it is capturing. A process is
4
+ // only ever signalled or trusted when its command line proves it is
5
+ // console-tail.mjs writing this runtime's log with this runtime's owner marker.
6
+
7
+ import { spawnSync } from 'node:child_process';
8
+ import { createHash, randomUUID } from 'node:crypto';
9
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ export const COLLECTOR_SCRIPT_NAME = 'console-tail.mjs';
14
+ // Logged in the app page to prove its console reaches the capture file.
15
+ export const CAPTURE_CONTROL_MARK = '[mm-harness-capture-control]';
16
+
17
+ // Same derivation as ownerMarker('console-collector', …) in
18
+ // adapters/terminal/lib/processes.mjs, so the terminal stop path finds it.
19
+ export function collectorOwnerMarker(runtimeDir) {
20
+ const id = createHash('sha256').update(path.resolve(runtimeDir)).digest('hex').slice(0, 32);
21
+ return `--mm-harness-owner=console-collector-${id}`;
22
+ }
23
+
24
+ export function processCommand(pid) {
25
+ if (!Number.isInteger(pid) || pid <= 0) return null;
26
+ try {
27
+ process.kill(pid, 0);
28
+ } catch {
29
+ return null;
30
+ }
31
+ const result = spawnSync('ps', ['-ww', '-p', String(pid), '-o', 'command='], { encoding: 'utf8' });
32
+ const command = result.status === 0 ? result.stdout.trim() : '';
33
+ return command || null;
34
+ }
35
+
36
+ export function readPid(pidFile) {
37
+ try {
38
+ const pid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
39
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ // This checkout's collector script: the only script a process may run to be
46
+ // adopted as this runtime's collector.
47
+ export const COLLECTOR_SCRIPT_PATH = fileURLToPath(new URL('../../../adapters/extension/console-tail.mjs', import.meta.url));
48
+
49
+ // A process's command line and the script it runs (its argv[1]), or null.
50
+ // Linux: /proc/<pid>/cmdline, exact. macOS: ps shows the arguments joined
51
+ // with spaces (a path may contain spaces), so argv[1] is what follows
52
+ // argv[0] (ps comm) up to " --cdp-port "; an argv[1] starting with "-" is a
53
+ // node option (node -e …), never a script.
54
+ export function processArgs(pid) {
55
+ const command = processCommand(pid);
56
+ if (!command) return null;
57
+ let script = null;
58
+ let args = null;
59
+ try {
60
+ const argv = readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter((arg, index, all) => index < all.length - 1 || arg);
61
+ script = argv[1] ?? null;
62
+ args = argv.slice(1).join(' ');
63
+ } catch {
64
+ const comm = spawnSync('ps', ['-ww', '-p', String(pid), '-o', 'comm='], { encoding: 'utf8' }).stdout?.trim();
65
+ if (comm && command.startsWith(`${comm} `)) {
66
+ args = command.slice(comm.length + 1);
67
+ const end = args.indexOf(' --cdp-port ');
68
+ script = end > 0 ? args.slice(0, end) : null;
69
+ }
70
+ }
71
+ // args: everything from argv[1] on, joined with spaces.
72
+ return script && !script.startsWith('-') ? { command, script, args } : { command, script: null, args: null };
73
+ }
74
+
75
+ function sameFile(a, b) {
76
+ try {
77
+ return realpathSync(a) === realpathSync(b);
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ // The command line (ps joins the arguments with spaces and a path may
84
+ // contain spaces, so it is never split on whitespace): each value is matched
85
+ // together with the argument that follows it, in the order
86
+ // ensureConsoleCollector starts the collector:
87
+ // <node> <dir>/console-tail.mjs --cdp-port <port> --extension-log <log>
88
+ // --dapp-log <log> <owner marker>
89
+ const writesExtensionLog = (command, extensionLog) => command.includes(` --extension-log ${extensionLog} --dapp-log `);
90
+
91
+ // This runtime's collector: running this checkout's console-tail.mjs (argv[1],
92
+ // resolved), writing this extension log, with this runtime's owner marker.
93
+ export function isOwnedCollector(proc, { extensionLog, marker, script = COLLECTOR_SCRIPT_PATH }) {
94
+ return Boolean(proc?.script) && sameFile(proc.script, script) && writesExtensionLog(proc.command, extensionLog) && proc.command.endsWith(` ${marker}`);
95
+ }
96
+
97
+ // A collector started before collectors carried owner markers, writing this
98
+ // runtime's extension log (that path lies in this runtime's directory): its
99
+ // argv[1] is a console-tail.mjs, from whichever harness started it.
100
+ export function isLegacyCollector(proc, { extensionLog }) {
101
+ return Boolean(proc?.script) && path.basename(proc.script) === COLLECTOR_SCRIPT_NAME &&
102
+ writesExtensionLog(proc.command, extensionLog) && !proc.command.includes('--mm-harness-owner=');
103
+ }
104
+
105
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
106
+
107
+ // This runtime's collector started by another install of the harness (the
108
+ // installed mm-harness and an MM_HARNESS_BIN checkout, say): argv[1] is a
109
+ // console-tail.mjs other than this checkout's, and the rest of the arguments
110
+ // are exactly the sequence a harness starts it with, for this runtime's
111
+ // extension log and owner marker. It is retired before a replacement starts;
112
+ // a process failing any of this (node -e, another runtime) is left alone.
113
+ export function isPriorInstallCollector(proc, { extensionLog, marker, script = COLLECTOR_SCRIPT_PATH }) {
114
+ if (!proc?.script || !proc.args || path.basename(proc.script) !== COLLECTOR_SCRIPT_NAME || sameFile(proc.script, script)) return false;
115
+ return new RegExp(`^${escapeRegExp(proc.script)} --cdp-port \\d+ --extension-log ${escapeRegExp(extensionLog)} --dapp-log \\S(?:.*\\S)? ${escapeRegExp(marker)}$`, 'u').test(proc.args);
116
+ }
117
+
118
+ // ...on this CDP port and these logs: the whole argument list, exactly.
119
+ export function collectorMatches(proc, { script = COLLECTOR_SCRIPT_PATH, cdpPort, extensionLog, pageLog, marker }) {
120
+ return isOwnedCollector(proc, { extensionLog, marker, script }) &&
121
+ proc.command.endsWith(` ${proc.script} --cdp-port ${cdpPort} --extension-log ${extensionLog} --dapp-log ${pageLog} ${marker}`);
122
+ }
123
+
124
+ // Is this runtime's collector running (on this CDP port)? { ok, pid, detail }
125
+ export function collectorStatus({ pidFile, extensionLog, marker, cdpPort, script = COLLECTOR_SCRIPT_PATH }) {
126
+ const pid = readPid(pidFile);
127
+ if (!pid) return { ok: false, pid: null, detail: `no console collector pid in ${pidFile}` };
128
+ const proc = processArgs(pid);
129
+ if (!proc) return { ok: false, pid, detail: `console collector pid ${pid} is not running` };
130
+ if (!isOwnedCollector(proc, { extensionLog, marker, script })) return { ok: false, pid, detail: `pid ${pid} is not this runtime's console collector` };
131
+ if (cdpPort && !proc.command.includes(` --cdp-port ${cdpPort} --extension-log `)) {
132
+ return { ok: false, pid, detail: `console collector pid ${pid} watches another CDP port than ${cdpPort}` };
133
+ }
134
+ return { ok: true, pid, detail: `console collector pid ${pid} on CDP ${cdpPort ?? '?'}` };
135
+ }
136
+
137
+ function originOf(url) {
138
+ try {
139
+ return new URL(String(url)).origin;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function fileSize(file) {
146
+ try {
147
+ return statSync(file).size;
148
+ } catch {
149
+ return 0;
150
+ }
151
+ }
152
+
153
+ function evaluateOnce(webSocketUrl, expression, timeoutMs) {
154
+ return new Promise((resolve, reject) => {
155
+ const socket = new WebSocket(webSocketUrl);
156
+ const timer = setTimeout(() => {
157
+ try { socket.close(); } catch { /* closing */ }
158
+ reject(new Error('timed out'));
159
+ }, timeoutMs);
160
+ const done = (error) => {
161
+ clearTimeout(timer);
162
+ try { socket.close(); } catch { /* closing */ }
163
+ if (error) reject(error);
164
+ else resolve();
165
+ };
166
+ socket.addEventListener('open', () => socket.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { expression } })));
167
+ socket.addEventListener('message', (event) => {
168
+ let message;
169
+ try {
170
+ message = JSON.parse(String(event.data));
171
+ } catch {
172
+ return;
173
+ }
174
+ if (message.id === 1) done(message.error ? new Error('the page refused the evaluation') : null);
175
+ });
176
+ socket.addEventListener('error', () => done(new Error('CDP connection failed')));
177
+ });
178
+ }
179
+
180
+ // The control check: log a marked line in a target the collector should be
181
+ // attached to and wait for it in that target's capture file. Proves the
182
+ // capture works now, for that file. { ok, detail }
183
+ // page an http(s) page on appOrigin -> the page log
184
+ // extension MetaMask's service worker or one of its pages, by MetaMask's
185
+ // extension id (another extension, such as Chrome's own
186
+ // component extensions, proves nothing) -> the extension log
187
+ // Details name only the expected target and log file, never values read from
188
+ // the browser.
189
+ export async function verifyCapture({ cdpPort, log, target: kind = 'page', appOrigin = null, extensionId = null, timeoutMs = 8000 }) {
190
+ const port = Number.parseInt(String(cdpPort), 10);
191
+ if (!(port > 0 && port < 65536)) return { ok: false, detail: `invalid CDP port ${JSON.stringify(cdpPort)}` };
192
+ if (kind === 'extension' && !/^[a-p]{32}$/u.test(String(extensionId ?? ''))) {
193
+ return { ok: false, detail: 'no MetaMask extension id is recorded for this slot (injected signer, or MetaMask was never launched)' };
194
+ }
195
+ const origin = kind === 'page' ? originOf(appOrigin) : null;
196
+ const label = kind === 'page' ? `${origin} page` : 'MetaMask extension';
197
+ let targets;
198
+ try {
199
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(3000) });
200
+ targets = await response.json();
201
+ } catch (error) {
202
+ return { ok: false, detail: `CDP ${port} is not reachable to verify the ${label} console capture (${error.name})` };
203
+ }
204
+ const candidates = (Array.isArray(targets) ? targets : []).filter((target) => target.webSocketDebuggerUrl && (kind === 'page'
205
+ ? target.type === 'page' && originOf(target.url) === origin
206
+ : ['service_worker', 'page'].includes(target.type) && String(target.url).startsWith(`chrome-extension://${extensionId}/`)));
207
+ // The service worker first: it lives as long as the extension.
208
+ const chosen = candidates.find((target) => target.type === 'service_worker') ?? candidates[0];
209
+ if (!chosen) return { ok: false, detail: `no ${label} target is open to verify the console capture` };
210
+ const nonce = randomUUID();
211
+ const start = fileSize(log);
212
+ try {
213
+ await evaluateOnce(chosen.webSocketDebuggerUrl, `console.info(${JSON.stringify(`${CAPTURE_CONTROL_MARK} ${nonce}`)})`, 3000);
214
+ } catch (error) {
215
+ return { ok: false, detail: `could not log the capture control line in the ${label} (${error.message})` };
216
+ }
217
+ const deadline = Date.now() + timeoutMs;
218
+ while (Date.now() < deadline) {
219
+ if (existsSync(log)) {
220
+ const text = readFileSync(log);
221
+ if (text.subarray(Math.min(start, text.length)).includes(nonce)) return { ok: true, detail: `control line from the ${label} reached ${path.basename(log)}` };
222
+ }
223
+ await new Promise((resolve) => setTimeout(resolve, 200));
224
+ }
225
+ return { ok: false, detail: `the capture control line logged in the ${label} did not reach ${log} within ${timeoutMs}ms` };
226
+ }
227
+
228
+ export function verifyPageCapture({ cdpPort, pageLog, appOrigin, timeoutMs }) {
229
+ return verifyCapture({ cdpPort, log: pageLog, target: 'page', appOrigin, timeoutMs });
230
+ }