@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
|
@@ -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
|
+
}
|