@deeeed/metamask-harness 0.80.0 → 0.82.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 +43 -0
- package/adapters/manifest.json +48 -64
- package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
- package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
- package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
- package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
- package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
- package/adapters/web-dapp/lib/policy.mjs +79 -0
- package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
- package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
- package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
- package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
- package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
- package/dist/adapters/core/actions.js +4 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/network-observer.js +13 -0
- package/dist/adapters/extension/run.js +115 -31
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +1 -3
- package/dist/adapters/mobile/run.js +32 -9
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/plugin-surface.js +98 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/surface.js +1 -1
- package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
- package/dist/adapters/web-dapp/console-capture.js +63 -0
- package/dist/adapters/web-dapp/doctor-checks.js +33 -0
- package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
- package/dist/adapters/web-dapp/overlay.js +21 -0
- package/dist/adapters/web-dapp/readiness.js +10 -0
- package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
- package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
- package/dist/adapters/web-dapp/surface.js +149 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +34 -6
- package/dist/command-contract.js +29 -7
- package/dist/commands/check.js +32 -8
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/doctor.js +19 -2
- package/dist/commands/farmslot-ready.js +1 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/launch/device.js +5 -5
- package/dist/commands/self-test.js +10 -5
- package/dist/engine.js +47 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/manifest.js +48 -24
- package/dist/mm-harness-cli.js +20 -3
- package/dist/performance-observation.js +7 -15
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +28 -8
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -3
- package/docs/cli-contract.md +5 -3
- package/library/actions/core/perps/_controller.mjs +24 -28
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +7 -4
- package/library/actions/core/wallet/list_accounts.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +71 -3
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
- package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
- package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
- package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
- package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
- package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
- package/library/actions/web-dapp/ui/navigate.mjs +24 -0
- package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
- package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
- package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
- package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
- package/library/manifests/core.action-manifest.json +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
- package/package.json +6 -6
- package/site/assets/help-recipes.json +1 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/adapters/terminal/console-capture.js +0 -59
- package/dist/adapters/terminal/doctor-checks.js +0 -22
- package/dist/adapters/terminal/overlay.js +0 -18
- package/dist/adapters/terminal/readiness.js +0 -11
- package/dist/adapters/terminal/surface.js +0 -133
- package/dist/commands/call.js +0 -671
- package/dist/commands/list-executables.js +0 -245
- package/dist/commands/manifest.js +0 -477
- package/dist/commands/run-engine.js +0 -1301
- package/dist/commands/run.js +0 -730
- package/dist/discovery.js +0 -15
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- package/library/actions/terminal/perps/_venue.mjs +0 -242
- package/library/actions/terminal/perps/assert_orders.mjs +0 -54
- package/library/actions/terminal/perps/assert_positions.mjs +0 -40
- package/library/actions/terminal/perps/read_orders.mjs +0 -26
- package/library/actions/terminal/perps/read_positions.mjs +0 -25
- package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
- package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
- package/library/actions/terminal/perps/teardown_state.mjs +0 -104
- package/library/actions/terminal/ui/navigate.mjs +0 -31
- package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
- package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// wallet-host.mjs — long-running observer (and, in injected mode, signer) for the
|
|
3
|
-
//
|
|
3
|
+
// web-dapp slot browser.
|
|
4
4
|
//
|
|
5
5
|
// Purpose:
|
|
6
6
|
// Attaches to the slot browser over CDP and, for every app document, installs
|
|
@@ -28,13 +28,14 @@ import { appendFileSync, chmodSync, existsSync, unwatchFile, watchFile, writeFil
|
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
|
|
31
|
+
import { createStrictWallet, createWalletRequestBinding, pageReadyExpression, pageScriptSource } from '@farmslot/adapter-web/dapp';
|
|
32
|
+
import { isAppUrl as isExactAppUrl, shortUrl } from '@farmslot/adapter-web/origin';
|
|
33
|
+
|
|
31
34
|
import { CdpClient, browserWebSocketUrl } from './lib/cdp-client.mjs';
|
|
32
35
|
import { hudFile, hudRenderExpression, readHudState } from './lib/hud.mjs';
|
|
33
|
-
import {
|
|
36
|
+
import { fixtureAccount, webDappPolicy } from '../../library/actions/web-dapp/platform/runtime.mjs';
|
|
37
|
+
import { hostOf, isBlockedUrl } from './lib/blocked-hosts.mjs';
|
|
34
38
|
import { ownedBrowserPids } from './lib/processes.mjs';
|
|
35
|
-
import { isAppTopFrameContext, isAppUrl as isExactAppUrl } from './lib/origin.mjs';
|
|
36
|
-
import { LOG_BINDING, REQUEST_BINDING, RESOLVE_FN, pageScriptSource } from './lib/page-script.mjs';
|
|
37
|
-
import { createStrictWallet } from './lib/strict-wallet.mjs';
|
|
38
39
|
|
|
39
40
|
const { captureMacFrontmost, macFocusDisabled } = createRequire(import.meta.url)('@farmslot/adapter-web/macos-focus');
|
|
40
41
|
|
|
@@ -65,6 +66,10 @@ function parseArgs(argv) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const args = parseArgs(process.argv.slice(2));
|
|
69
|
+
// The venue policy (e.g. the Terminal plugin's policy.mjs), from RECIPE_WEB_DAPP_POLICY:
|
|
70
|
+
// link hosts, the probe, the typed-data refusal, the start page and chain.
|
|
71
|
+
const policy = webDappPolicy();
|
|
72
|
+
const LINK_HOSTS = new Set(policy.linkHosts);
|
|
68
73
|
const runtimeDir = path.resolve(args['runtime-dir']);
|
|
69
74
|
const logFile = path.join(runtimeDir, 'wallet-requests.jsonl');
|
|
70
75
|
const signer = args.signer;
|
|
@@ -79,6 +84,19 @@ if (network === 'testnet' && mainnetHosts.length === 0) throw new Error('testnet
|
|
|
79
84
|
// only, never a link host (app.hyperliquid-testnet.xyz serves pages and icons).
|
|
80
85
|
const testnetHosts = String(args['testnet-hosts'] ?? '').split(',').filter((host) => host && !LINK_HOSTS.has(host));
|
|
81
86
|
const PROBE_MARK = 'mm-harness-probe=1';
|
|
87
|
+
// A testnet run never lets the typed data the policy refuses reach the signer.
|
|
88
|
+
const refuseTypedData = network === 'testnet' ? policy.refuseTypedData : null;
|
|
89
|
+
// The injected strict wallet presents as MetaMask, so the app connects to it
|
|
90
|
+
// the way it connects to the extension.
|
|
91
|
+
const INJECTED_WALLET = {
|
|
92
|
+
info: {
|
|
93
|
+
uuid: '5d1e5b4e-e2e0-4c3f-9a1d-000000000001',
|
|
94
|
+
name: 'MetaMask (strict test wallet)',
|
|
95
|
+
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
96
|
+
rdns: 'io.metamask',
|
|
97
|
+
},
|
|
98
|
+
isMetaMask: true,
|
|
99
|
+
};
|
|
82
100
|
|
|
83
101
|
// The request log is evidence; keep it owner-only even when it already exists.
|
|
84
102
|
if (existsSync(logFile)) chmodSync(logFile, 0o600);
|
|
@@ -93,32 +111,25 @@ function say(message) {
|
|
|
93
111
|
|
|
94
112
|
let wallet = null;
|
|
95
113
|
if (signer === 'injected') {
|
|
96
|
-
const { fixtureAccount } = await import('../../library/actions/terminal/platform/runtime.mjs');
|
|
97
114
|
if (!args.target || !args.account) throw new Error('injected mode requires --target and --account');
|
|
98
115
|
const account = await fixtureAccount(path.resolve(args.target), args.account);
|
|
99
|
-
wallet = createStrictWallet({ account, chainId: Number(args['start-chain'] ??
|
|
116
|
+
wallet = createStrictWallet({ account, chainId: Number(args['start-chain'] ?? policy.startChain) });
|
|
100
117
|
say(`injected strict wallet ready for ${args.account} (${account.address}) on chain ${wallet.chainId}`);
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
const source = pageScriptSource({ signer, appOrigin,
|
|
120
|
+
const source = pageScriptSource({ signer, appOrigin, refuseTypedData, injectedWallet: INJECTED_WALLET });
|
|
104
121
|
const client = await CdpClient.connect(await browserWebSocketUrl(Number(args['cdp-port'])));
|
|
122
|
+
// Judges each binding call against the frame and document that made it, logs
|
|
123
|
+
// it and, in injected mode, answers it with the strict wallet.
|
|
124
|
+
const binding = createWalletRequestBinding({ client, appOrigin, signer, wallet, refuseTypedData, record: appendEntry, say });
|
|
105
125
|
// Hooks are installed once per target: a second session for the same tab
|
|
106
126
|
// (auto-attach plus a restored tab) would log every request twice.
|
|
107
127
|
const hookedTargets = new Map();
|
|
108
128
|
const appSessions = new Set();
|
|
109
|
-
// Per app session: its target id (= main frame id)
|
|
110
|
-
// committed
|
|
111
|
-
// the frame, document and origin that made it.
|
|
129
|
+
// Per app session: its target id (= main frame id) and the URL its top frame
|
|
130
|
+
// has committed.
|
|
112
131
|
const sessionTargets = new Map();
|
|
113
132
|
const sessionCommittedUrls = new Map();
|
|
114
|
-
const sessionContexts = new Map();
|
|
115
|
-
// Binding calls whose execution context, or that context's document, is not
|
|
116
|
-
// known yet.
|
|
117
|
-
const pendingCalls = new Map();
|
|
118
|
-
// Per app session: the URL of a top-frame commit no execution context has
|
|
119
|
-
// been tied to yet.
|
|
120
|
-
const awaitingDocuments = new Map();
|
|
121
|
-
const PENDING_CALL_TTL_MS = 5000;
|
|
122
133
|
// App sessions whose target runs with domains enabled; the HUD is drawn only
|
|
123
134
|
// there so no evaluate ever waits on a tab paused for setup.
|
|
124
135
|
const hudSessions = new Set();
|
|
@@ -129,41 +140,6 @@ function isAppUrl(url) {
|
|
|
129
140
|
return url === 'about:blank' || url === '' || isExactAppUrl(url, appOrigin);
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
function isTopDefaultContext(sessionId, context) {
|
|
133
|
-
return context?.auxData?.isDefault === true && context.auxData.frameId === sessionTargets.get(sessionId);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// A top-frame commit is tied to the top frame's default context of its
|
|
137
|
-
// document: one created before the commit and still untied, or else the next
|
|
138
|
-
// one created.
|
|
139
|
-
// A commit is applied once: Page.getFrameTree and Page.frameNavigated can
|
|
140
|
-
// both report the same document (its loaderId); a second report would leave a
|
|
141
|
-
// stale commit waiting for the next document's context.
|
|
142
|
-
const appliedCommits = new Map();
|
|
143
|
-
function assignDocumentUrl(sessionId, url, loaderId) {
|
|
144
|
-
if (loaderId) {
|
|
145
|
-
if (appliedCommits.get(sessionId) === loaderId) return;
|
|
146
|
-
appliedCommits.set(sessionId, loaderId);
|
|
147
|
-
}
|
|
148
|
-
const untied = [...(sessionContexts.get(sessionId)?.values() ?? [])]
|
|
149
|
-
.filter((context) => isTopDefaultContext(sessionId, context) && context.documentUrl === null);
|
|
150
|
-
if (untied.length === 0) {
|
|
151
|
-
awaitingDocuments.set(sessionId, url);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
for (const context of untied) context.documentUrl = url;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Judged against the caller's own document, not the tab's latest commit.
|
|
158
|
-
function callerIsAppTopFrame(sessionId, executionContextId) {
|
|
159
|
-
const context = sessionContexts.get(sessionId)?.get(executionContextId);
|
|
160
|
-
return isAppTopFrameContext(context, {
|
|
161
|
-
targetId: sessionTargets.get(sessionId),
|
|
162
|
-
appOrigin,
|
|
163
|
-
committedUrl: context?.documentUrl ?? null,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
143
|
// MetaMask shows dapp requests in its side panel (the fixture default) or a
|
|
168
144
|
// notification/popup window; only request routes count as confirmations.
|
|
169
145
|
const WALLET_SURFACES = ['sidepanel.html', 'notification.html', 'popup.html'];
|
|
@@ -233,9 +209,7 @@ function bounded(promise, label, ms = 10000) {
|
|
|
233
209
|
async function installAppHooks(sessionId, targetId) {
|
|
234
210
|
appSessions.add(sessionId);
|
|
235
211
|
sessionTargets.set(sessionId, targetId);
|
|
236
|
-
|
|
237
|
-
await bounded(client.send('Runtime.addBinding', { name: LOG_BINDING }, sessionId), 'Runtime.addBinding');
|
|
238
|
-
if (signer === 'injected') await bounded(client.send('Runtime.addBinding', { name: REQUEST_BINDING }, sessionId), 'Runtime.addBinding');
|
|
212
|
+
await bounded(binding.install(sessionId, targetId), 'Runtime.addBinding');
|
|
239
213
|
await bounded(client.send('Page.addScriptToEvaluateOnNewDocument', { source, runImmediately: true }, sessionId), 'Page.addScriptToEvaluateOnNewDocument');
|
|
240
214
|
if (network === 'testnet') {
|
|
241
215
|
await bounded(client.send('Fetch.enable', {
|
|
@@ -244,16 +218,6 @@ async function installAppHooks(sessionId, targetId) {
|
|
|
244
218
|
}
|
|
245
219
|
}
|
|
246
220
|
|
|
247
|
-
// Where a request went, without its query string or body.
|
|
248
|
-
function shortUrl(url) {
|
|
249
|
-
try {
|
|
250
|
-
const parsed = new URL(url);
|
|
251
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
252
|
-
} catch {
|
|
253
|
-
return String(url).slice(0, 120);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
221
|
// Venue traffic per app session: the served-network check. Only venue
|
|
258
222
|
// endpoints count (the block list and the testnet endpoints); other hosts on a
|
|
259
223
|
// venue domain, such as the coin icons on app.hyperliquid.xyz, are ignored.
|
|
@@ -288,10 +252,10 @@ async function enableAppDomains(sessionId) {
|
|
|
288
252
|
const { frameTree } = await bounded(client.send('Page.getFrameTree', {}, sessionId), 'Page.getFrameTree');
|
|
289
253
|
if (frameTree?.frame?.url !== undefined) {
|
|
290
254
|
sessionCommittedUrls.set(sessionId, frameTree.frame.url);
|
|
291
|
-
|
|
255
|
+
binding.commit(sessionId, frameTree.frame.url, frameTree.frame.loaderId);
|
|
292
256
|
}
|
|
293
257
|
if (appSessions.has(sessionId)) hudSessions.add(sessionId);
|
|
294
|
-
|
|
258
|
+
binding.drain(sessionId);
|
|
295
259
|
}
|
|
296
260
|
|
|
297
261
|
// Redraw the HUD on app pages. The expression is a no-op when the same state
|
|
@@ -344,18 +308,13 @@ client.on('Target.targetCreated', ({ targetInfo }) => {
|
|
|
344
308
|
if (targetInfo.type === 'page') observeWalletSurface(targetInfo.targetId, targetInfo.url);
|
|
345
309
|
});
|
|
346
310
|
client.on('Target.detachedFromTarget', ({ sessionId }) => {
|
|
347
|
-
|
|
348
|
-
for (const call of pendingCalls.get(sessionId) ?? []) recordUnattributed(call, 'its tab detached first');
|
|
349
|
-
awaitingDocuments.delete(sessionId);
|
|
350
|
-
appliedCommits.delete(sessionId);
|
|
311
|
+
binding.detach(sessionId);
|
|
351
312
|
const targetId = sessionTargets.get(sessionId);
|
|
352
313
|
if (targetId && hookedTargets.get(targetId)?.sessionId === sessionId) hookedTargets.delete(targetId);
|
|
353
314
|
appSessions.delete(sessionId);
|
|
354
315
|
hudSessions.delete(sessionId);
|
|
355
316
|
sessionTargets.delete(sessionId);
|
|
356
317
|
sessionCommittedUrls.delete(sessionId);
|
|
357
|
-
sessionContexts.delete(sessionId);
|
|
358
|
-
pendingCalls.delete(sessionId);
|
|
359
318
|
});
|
|
360
319
|
// What the app tab's first navigation did, for the diagnostic when it never
|
|
361
320
|
// commits: main-frame commits, and the document's response or failure.
|
|
@@ -370,12 +329,11 @@ client.on('Page.frameNavigated', ({ frame }, sessionId) => {
|
|
|
370
329
|
if (sessionId === appNavigation.session && !frame.parentId) appNavigation.commits.push(pathOf(frame.url));
|
|
371
330
|
if (appSessions.has(sessionId) && !frame.parentId) {
|
|
372
331
|
sessionCommittedUrls.set(sessionId, frame.url);
|
|
373
|
-
|
|
374
|
-
drainPendingCalls(sessionId);
|
|
332
|
+
binding.commit(sessionId, frame.url, frame.loaderId);
|
|
375
333
|
}
|
|
376
334
|
});
|
|
377
335
|
client.on('Fetch.requestPaused', ({ requestId, request, resourceType }, sessionId) => {
|
|
378
|
-
const mainnet =
|
|
336
|
+
const mainnet = isBlockedUrl(request.url, mainnetHosts);
|
|
379
337
|
if (mainnet) {
|
|
380
338
|
appendEntry({ kind: 'blocked-mainnet', t: Date.now(), transport: 'http', url: shortUrl(request.url), resourceType, probe: request.url.includes(PROBE_MARK) });
|
|
381
339
|
say(`blocked a mainnet request to ${shortUrl(request.url)}`);
|
|
@@ -402,30 +360,11 @@ client.on('Network.webSocketCreated', ({ requestId, url }, sessionId) => {
|
|
|
402
360
|
if (url.includes(PROBE_MARK)) probeRequests.set(requestId, 'websocket');
|
|
403
361
|
recordVenueTraffic(sessionId, url);
|
|
404
362
|
// The host resolver rule fails the connection; the attempt is still evidence.
|
|
405
|
-
if (
|
|
363
|
+
if (isBlockedUrl(url, mainnetHosts)) {
|
|
406
364
|
appendEntry({ kind: 'blocked-mainnet', t: Date.now(), transport: 'websocket', url: shortUrl(url), probe: url.includes(PROBE_MARK) });
|
|
407
365
|
say(`blocked a mainnet WebSocket to ${shortUrl(url)}`);
|
|
408
366
|
}
|
|
409
367
|
});
|
|
410
|
-
client.on('Runtime.executionContextCreated', ({ context }, sessionId) => {
|
|
411
|
-
const contexts = sessionContexts.get(sessionId);
|
|
412
|
-
if (!contexts) return;
|
|
413
|
-
contexts.set(context.id, context);
|
|
414
|
-
if (isTopDefaultContext(sessionId, context)) {
|
|
415
|
-
// The navigation this document belongs to may already have committed
|
|
416
|
-
// (frameNavigated first) or not yet (the context first).
|
|
417
|
-
const awaiting = awaitingDocuments.get(sessionId);
|
|
418
|
-
context.documentUrl = awaiting ?? null;
|
|
419
|
-
awaitingDocuments.delete(sessionId);
|
|
420
|
-
}
|
|
421
|
-
drainPendingCalls(sessionId);
|
|
422
|
-
});
|
|
423
|
-
client.on('Runtime.executionContextDestroyed', ({ executionContextId }, sessionId) => {
|
|
424
|
-
sessionContexts.get(sessionId)?.delete(executionContextId);
|
|
425
|
-
});
|
|
426
|
-
client.on('Runtime.executionContextsCleared', (_params, sessionId) => {
|
|
427
|
-
sessionContexts.get(sessionId)?.clear();
|
|
428
|
-
});
|
|
429
368
|
for (const event of ['Page.domContentEventFired', 'Page.loadEventFired', 'Page.navigatedWithinDocument']) {
|
|
430
369
|
client.on(event, (_params, sessionId) => {
|
|
431
370
|
if (hudSessions.has(sessionId)) drawHud([sessionId]);
|
|
@@ -436,123 +375,6 @@ for (const event of ['Page.domContentEventFired', 'Page.loadEventFired', 'Page.n
|
|
|
436
375
|
watchFile(hudFile(runtimeDir), { interval: 500 }, () => drawHud());
|
|
437
376
|
const hudRepeat = setInterval(() => drawHud(), 2000);
|
|
438
377
|
|
|
439
|
-
// Answer a request the host refuses, so the page's promise settles instead of
|
|
440
|
-
// hanging; harmless where the preload (and its resolver) is absent.
|
|
441
|
-
function refuse(sessionId, executionContextId, id, message) {
|
|
442
|
-
const outcome = { error: { message, code: 4100 } };
|
|
443
|
-
const expression = `typeof window.${RESOLVE_FN} === 'function' && window.${RESOLVE_FN}(${JSON.stringify(id)}, ${JSON.stringify(outcome)})`;
|
|
444
|
-
return client.send('Runtime.evaluate', { expression, contextId: executionContextId }, sessionId).catch(() => {});
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function requestId(name, payload) {
|
|
448
|
-
if (name !== REQUEST_BINDING) return null;
|
|
449
|
-
try {
|
|
450
|
-
return JSON.parse(payload).id ?? null;
|
|
451
|
-
} catch {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function handleBindingCall({ name, payload, executionContextId }, sessionId) {
|
|
457
|
-
// Only the app's own top frame may log or ask the injected wallet to sign;
|
|
458
|
-
// anything else (an iframe, a blank popup, a page the tab navigated to) is
|
|
459
|
-
// dropped, and a refused request is answered with 4100.
|
|
460
|
-
if (!callerIsAppTopFrame(sessionId, executionContextId)) {
|
|
461
|
-
const context = sessionContexts.get(sessionId)?.get(executionContextId);
|
|
462
|
-
appendEntry({ kind: 'outside-app-frame', t: Date.now(), binding: name === REQUEST_BINDING ? 'request' : 'log', origin: context?.origin ?? null, document: context?.documentUrl ? shortUrl(context.documentUrl) : null });
|
|
463
|
-
say(`ignored ${name === REQUEST_BINDING ? 'wallet request' : 'log entry'} from outside the app top frame`);
|
|
464
|
-
const id = requestId(name, payload);
|
|
465
|
-
if (id !== null) await refuse(sessionId, executionContextId, id, 'Wallet requests are accepted only from the app top frame.');
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (name === LOG_BINDING) {
|
|
469
|
-
try {
|
|
470
|
-
appendEntry(JSON.parse(payload));
|
|
471
|
-
} catch (error) {
|
|
472
|
-
say(`unparseable log payload: ${error.message}`);
|
|
473
|
-
}
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (name !== REQUEST_BINDING || !wallet) return;
|
|
477
|
-
const { id, method, params } = JSON.parse(payload);
|
|
478
|
-
if (network === 'testnet' && /^eth_signTypedData/u.test(String(method))) {
|
|
479
|
-
let reason = null;
|
|
480
|
-
try {
|
|
481
|
-
reason = mainnetTypedDataReason(typeof params?.[1] === 'string' ? JSON.parse(params[1]) : params?.[1]);
|
|
482
|
-
} catch {
|
|
483
|
-
reason = null;
|
|
484
|
-
}
|
|
485
|
-
if (reason) {
|
|
486
|
-
appendEntry({ kind: 'refused-mainnet', t: Date.now(), method, reason, layer: 'wallet-host' });
|
|
487
|
-
await refuse(sessionId, executionContextId, id, 'Refused: this test run is held to Hyperliquid testnet and the request signs a mainnet action.');
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
let outcome;
|
|
492
|
-
try {
|
|
493
|
-
outcome = { result: await wallet.request({ method, params }) };
|
|
494
|
-
} catch (error) {
|
|
495
|
-
outcome = { error: { message: String(error.message), code: error.code ?? -32603 } };
|
|
496
|
-
}
|
|
497
|
-
await client.send(
|
|
498
|
-
'Runtime.evaluate',
|
|
499
|
-
{ expression: `window.${RESOLVE_FN}(${JSON.stringify(id)}, ${JSON.stringify(outcome)})`, contextId: executionContextId },
|
|
500
|
-
sessionId,
|
|
501
|
-
).catch((error) => say(`resolve ${method} failed: ${error.message}`));
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// A binding call can arrive before its execution context is reported (the
|
|
505
|
-
// domains are enabled after the tab resumes), or before its document's commit.
|
|
506
|
-
// Hold it until both are known; after a short wait it cannot be judged: it is
|
|
507
|
-
// logged as unattributed (and a request is refused).
|
|
508
|
-
function drainPendingCalls(sessionId) {
|
|
509
|
-
const queue = pendingCalls.get(sessionId);
|
|
510
|
-
if (!queue) return;
|
|
511
|
-
for (const call of [...queue]) {
|
|
512
|
-
const known = callAttributable(sessionId, call.executionContextId);
|
|
513
|
-
if (!known && Date.now() - call.at < PENDING_CALL_TTL_MS) continue;
|
|
514
|
-
queue.splice(queue.indexOf(call), 1);
|
|
515
|
-
if (known) {
|
|
516
|
-
handleBindingCall(call, sessionId);
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
recordUnattributed(call, 'its document was never attributed');
|
|
520
|
-
const id = requestId(call.name, call.payload);
|
|
521
|
-
if (id !== null) refuse(sessionId, call.executionContextId, id, 'Wallet request from a document the harness could not attribute.');
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function recordUnattributed(call, why) {
|
|
526
|
-
let entry = {};
|
|
527
|
-
try {
|
|
528
|
-
entry = JSON.parse(call.payload);
|
|
529
|
-
} catch {
|
|
530
|
-
entry = {};
|
|
531
|
-
}
|
|
532
|
-
const binding = call.name === REQUEST_BINDING ? 'request' : 'log';
|
|
533
|
-
appendEntry({ kind: 'unattributed', t: Date.now(), binding, method: typeof entry.method === 'string' ? entry.method : null, loggedKind: typeof entry.kind === 'string' ? entry.kind : null, reason: why });
|
|
534
|
-
say(`a wallet ${binding === 'request' ? 'request' : 'log entry'} could not be judged (${why}); recorded as unattributed`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// A binding call can be judged once its execution context is known and, for
|
|
538
|
-
// the top frame's default context, the URL of that context's own document.
|
|
539
|
-
function callAttributable(sessionId, executionContextId) {
|
|
540
|
-
const context = sessionContexts.get(sessionId)?.get(executionContextId);
|
|
541
|
-
if (!context) return false;
|
|
542
|
-
return !isTopDefaultContext(sessionId, context) || context.documentUrl !== null;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
client.on('Runtime.bindingCalled', (call, sessionId) => {
|
|
546
|
-
if (callAttributable(sessionId, call.executionContextId)) {
|
|
547
|
-
handleBindingCall(call, sessionId);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
const queue = pendingCalls.get(sessionId) ?? [];
|
|
551
|
-
queue.push({ ...call, at: Date.now() });
|
|
552
|
-
pendingCalls.set(sessionId, queue);
|
|
553
|
-
setTimeout(() => drainPendingCalls(sessionId), PENDING_CALL_TTL_MS + 50);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
378
|
client.on('close', () => {
|
|
557
379
|
clearInterval(hudRepeat);
|
|
558
380
|
unwatchFile(hudFile(runtimeDir));
|
|
@@ -632,7 +454,7 @@ for (const info of startupTabs) {
|
|
|
632
454
|
// The first app document is a market page: it streams from the venue in the
|
|
633
455
|
// browser, which the served-network check below observes (the home page's
|
|
634
456
|
// data comes from the server render).
|
|
635
|
-
const startUrl = new URL(args['start-path'] ||
|
|
457
|
+
const startUrl = new URL(args['start-path'] || policy.startPath, `${appOrigin}/`).href;
|
|
636
458
|
appNavigation.session = appSession;
|
|
637
459
|
const navigation = await bounded(client.send('Page.navigate', { url: startUrl }, appSession), 'Page.navigate', 60000).catch((error) => fail(error.message));
|
|
638
460
|
const navigateStart = Date.now();
|
|
@@ -652,13 +474,7 @@ const evaluate = (expression, timeoutMs = 5000) => bounded(
|
|
|
652
474
|
// The first app document must carry the log binding and the installed
|
|
653
475
|
// preload (its marker), with the page's provider wrapped, or requests on it
|
|
654
476
|
// would go unrecorded.
|
|
655
|
-
const readyExpression =
|
|
656
|
-
`typeof window.${LOG_BINDING} === 'function'`,
|
|
657
|
-
...(signer === 'injected' ? [`typeof window.${REQUEST_BINDING} === 'function'`] : []),
|
|
658
|
-
`Boolean(window.__mmTerminalHarness && window.__mmTerminalHarness.signer === ${JSON.stringify(signer)} && window.__mmTerminalHarness.network === ${JSON.stringify(network)})`,
|
|
659
|
-
// The provider the app sees is the wrapped one, and no wrap failed.
|
|
660
|
-
'window.__mmTerminalHarness.wrapFailures.length === 0 && window.__mmTerminalHarness.covers(window.ethereum)',
|
|
661
|
-
].join(' && ');
|
|
477
|
+
const readyExpression = pageReadyExpression({ signer, refusesTypedData: refuseTypedData !== null });
|
|
662
478
|
let hooksReady = false;
|
|
663
479
|
for (let attempt = 0; attempt < 50 && !hooksReady; attempt += 1) {
|
|
664
480
|
hooksReady = (await evaluate(readyExpression)) === true;
|
|
@@ -670,10 +486,11 @@ if (network === 'testnet') {
|
|
|
670
486
|
// The block must hold before anything runs: a probe to the mainnet venue
|
|
671
487
|
// from the app page has to fail (HTTP through the request block and the
|
|
672
488
|
// resolver rule, WebSocket through the resolver rule).
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
const
|
|
676
|
-
const
|
|
489
|
+
const { probe } = policy;
|
|
490
|
+
const host = mainnetHosts.includes(probe.host) ? probe.host : mainnetHosts[0];
|
|
491
|
+
const httpProbe = await evaluate(`fetch('https://${host}${probe.httpPath}?${PROBE_MARK}', { method: 'POST', headers: { 'content-type': 'application/json' }, body: ${JSON.stringify(probe.httpBody)} }).then(() => 'reached', () => 'blocked')`, 20000);
|
|
492
|
+
const resolverProbe = await evaluate(`fetch('https://${host}:${RESOLVER_PROBE_PORT}${probe.httpPath}?${PROBE_MARK}', { method: 'POST' }).then(() => 'reached', () => 'blocked')`, 20000);
|
|
493
|
+
const wsProbe = await evaluate(`new Promise((resolve) => { const socket = new WebSocket('wss://${host}${probe.wsPath}?${PROBE_MARK}'); const done = (value) => { try { socket.close(); } catch {} resolve(value); }; socket.onopen = () => done('reached'); socket.onerror = () => done('blocked'); setTimeout(() => done('timeout'), 10000); })`, 20000);
|
|
677
494
|
if (httpProbe !== 'blocked' || resolverProbe !== 'blocked' || wsProbe !== 'blocked') {
|
|
678
495
|
fail(`the mainnet venue is reachable from the app (http ${httpProbe}, resolver ${resolverProbe}, websocket ${wsProbe})`);
|
|
679
496
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { manifestPath } from "../../paths.js";
|
|
2
|
+
import { perpsOrderInputFindings } from "../perps-order-inputs.js";
|
|
2
3
|
const coreActions = {
|
|
3
4
|
manifestPath: () => manifestPath("core"),
|
|
4
5
|
semantic: [
|
|
@@ -14,7 +15,9 @@ const coreActions = {
|
|
|
14
15
|
env: async (projectRoot, tempDir) => (await import("./live-script-env.js")).coreLiveScriptEnv(projectRoot, tempDir)
|
|
15
16
|
},
|
|
16
17
|
// Core declares no cdp.target; the probe matches the Extension's.
|
|
17
|
-
cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" }
|
|
18
|
+
cdpTarget: { transport: "chrome-extension-cdp", probePath: "json/version" },
|
|
19
|
+
// ensure_positions and ensure_orders with an open or present state place an order.
|
|
20
|
+
inputFindings: (nodeId, node) => perpsOrderInputFindings(nodeId, node, { ensureChecksOnly: false })
|
|
18
21
|
};
|
|
19
22
|
export {
|
|
20
23
|
coreActions
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { manifestPath } from "../../paths.js";
|
|
2
|
+
import { perpsOrderInputFindings } from "../perps-order-inputs.js";
|
|
2
3
|
import { extensionActionInputFindings } from "./action-inputs.js";
|
|
3
4
|
const extensionActions = {
|
|
4
5
|
manifestPath: () => manifestPath("extension"),
|
|
@@ -43,8 +44,11 @@ const extensionActions = {
|
|
|
43
44
|
})
|
|
44
45
|
}),
|
|
45
46
|
trustedMutation: true,
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// ensure_positions and ensure_orders only check state here.
|
|
48
|
+
inputFindings: (nodeId, node) => [
|
|
49
|
+
...extensionActionInputFindings(nodeId, node),
|
|
50
|
+
...perpsOrderInputFindings(nodeId, node, { ensureChecksOnly: true })
|
|
51
|
+
]
|
|
48
52
|
};
|
|
49
53
|
export {
|
|
50
54
|
extensionActions
|
|
@@ -204,6 +204,19 @@ async function waitForSocket(socketPath) {
|
|
|
204
204
|
}
|
|
205
205
|
throw new Error("Extension network observer broker did not start.");
|
|
206
206
|
}
|
|
207
|
+
async function createExtensionNetworkBackend(env, artifactsDir) {
|
|
208
|
+
return createExtensionNetworkObserver(extensionCdpPort(env), artifactsDir);
|
|
209
|
+
}
|
|
210
|
+
function extensionCdpPort(env) {
|
|
211
|
+
const port = Number(
|
|
212
|
+
env.CDP_PORT ?? env.RECIPE_CDP_PORT ?? process.env.CDP_PORT
|
|
213
|
+
);
|
|
214
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
215
|
+
throw new Error("Extension network observation requires CDP_PORT.");
|
|
216
|
+
}
|
|
217
|
+
return port;
|
|
218
|
+
}
|
|
207
219
|
export {
|
|
220
|
+
createExtensionNetworkBackend,
|
|
208
221
|
createExtensionNetworkObserver
|
|
209
222
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { EXIT, optionFlag } from "@farmslot/recipe-cli/harness";
|
|
2
3
|
import { verifyOwnedCollectorControl } from "../console-capture.js";
|
|
3
4
|
import { resolveExtensionBrowserPid } from "./browser-pid.js";
|
|
4
5
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
extensionCdpPort,
|
|
8
9
|
verifyExtensionConsoleControl
|
|
9
10
|
} from "./console-capture.js";
|
|
11
|
+
import { emitProofFailure } from "../proof-failure.js";
|
|
10
12
|
import { launchedBrowser } from "./launched-browser.js";
|
|
11
13
|
const AUTOLAUNCH_ENV = "METAMASK_RECIPE_EXTENSION_AUTOLAUNCH";
|
|
12
14
|
const extensionRun = {
|
|
@@ -36,29 +38,9 @@ const extensionRun = {
|
|
|
36
38
|
pageMode: "home-or-sidepanel"
|
|
37
39
|
});
|
|
38
40
|
},
|
|
39
|
-
// The proof runtime is checked on the run's own ports
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return async () => {
|
|
43
|
-
const previousCdpPort = process.env.CDP_PORT;
|
|
44
|
-
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
45
|
-
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
46
|
-
const explicitCdpPort = optionString(options, "cdpPort");
|
|
47
|
-
const explicitWatcherPort = optionString(options, "watcherPort") ?? optionString(options, "metroPort");
|
|
48
|
-
if (explicitCdpPort) {
|
|
49
|
-
process.env.CDP_PORT = explicitCdpPort;
|
|
50
|
-
process.env.RECIPE_CDP_PORT = explicitCdpPort;
|
|
51
|
-
}
|
|
52
|
-
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
53
|
-
try {
|
|
54
|
-
const { ensureExtensionProofRuntime } = await import("../../commands/run-engine.js");
|
|
55
|
-
return await ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
|
|
56
|
-
} finally {
|
|
57
|
-
restoreEnv("CDP_PORT", previousCdpPort);
|
|
58
|
-
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
59
|
-
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
41
|
+
// The proof runtime is checked on the run's own ports (the engine's scope).
|
|
42
|
+
runtimeCheck({ target, heal, state, json, onRecovery }) {
|
|
43
|
+
return () => ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
|
|
62
44
|
},
|
|
63
45
|
launchedBrowser: (target, artifactsDir, cdpPort) => launchedBrowser(target, artifactsDir, cdpPort),
|
|
64
46
|
browserProvenance: (browser) => ({
|
|
@@ -68,6 +50,106 @@ const extensionRun = {
|
|
|
68
50
|
extension_loading: browser.extensionLoading
|
|
69
51
|
})
|
|
70
52
|
};
|
|
53
|
+
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
54
|
+
const decide = deps.decide ?? (async () => {
|
|
55
|
+
const { decideExtensionReadiness } = await import("./runtime-decision.js");
|
|
56
|
+
const port = Number(process.env.CDP_PORT);
|
|
57
|
+
return decideExtensionReadiness(target, {
|
|
58
|
+
...Number.isInteger(port) && port > 0 ? { cdpPort: port } : {},
|
|
59
|
+
pageMode: "home-or-sidepanel"
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
const initial = await decide();
|
|
63
|
+
if (initial.decision === "ready") return null;
|
|
64
|
+
if (initial.reasonCode === "release-artifact-identity-invalid" || initial.reasonCode === "release-artifact-runtime-missing" || initial.reasonCode === "release-artifact-runtime-stale") {
|
|
65
|
+
const message = initial.reasons[0] ?? "The loaded release artifact identity is invalid.";
|
|
66
|
+
const userAction2 = "rerun runtime-launch with the same artifact source";
|
|
67
|
+
emitProofFailure(json, {
|
|
68
|
+
recoverable: false,
|
|
69
|
+
error: { code: "RELEASE_ARTIFACT_NOT_CURRENT", message, userAction: userAction2 }
|
|
70
|
+
});
|
|
71
|
+
return EXIT.infra;
|
|
72
|
+
}
|
|
73
|
+
if (initial.reasonCode === "runtime-dist-check-failed") {
|
|
74
|
+
const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
|
|
75
|
+
const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
|
|
76
|
+
emitProofFailure(json, {
|
|
77
|
+
recoverable: false,
|
|
78
|
+
error: { code: "EXTENSION_RUNTIME_CHECK_FAILED", message, userAction: userAction2 }
|
|
79
|
+
});
|
|
80
|
+
return EXIT.infra;
|
|
81
|
+
}
|
|
82
|
+
if (![
|
|
83
|
+
"dist-stale",
|
|
84
|
+
"dist-missing",
|
|
85
|
+
"runtime-dist-stale",
|
|
86
|
+
"runtime-dist-missing"
|
|
87
|
+
].includes(initial.reasonCode)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const tier = initial.decision === "build" ? "build" : "quick";
|
|
91
|
+
const canHeal = heal === "auto" || heal === "infra-only" && tier === "quick";
|
|
92
|
+
const targetArg = JSON.stringify(path.resolve(target));
|
|
93
|
+
const userAction = tier === "build" ? `mm-harness launch --build --verify --adapter extension --target ${targetArg}` : `mm-harness launch --verify --adapter extension --target ${targetArg}`;
|
|
94
|
+
if (!canHeal) {
|
|
95
|
+
const message = `Extension proof runtime is not current (${initial.reasonCode}): ${initial.reasons[0] ?? initial.decision}`;
|
|
96
|
+
emitProofFailure(json, {
|
|
97
|
+
recoverable: true,
|
|
98
|
+
mutations: state.mutations,
|
|
99
|
+
error: { code: "EXTENSION_RUNTIME_NOT_CURRENT", message, userAction }
|
|
100
|
+
});
|
|
101
|
+
return EXIT.infra;
|
|
102
|
+
}
|
|
103
|
+
const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
|
|
104
|
+
const productConfigBlock = tier === "build" ? (await import("./product-config.js")).extensionProductConfigBlock(target) : null;
|
|
105
|
+
if (productConfigBlock) {
|
|
106
|
+
const message = productConfigBlock.message;
|
|
107
|
+
const userAction2 = productConfigBlock.userAction;
|
|
108
|
+
emitProofFailure(json, {
|
|
109
|
+
recoverable: false,
|
|
110
|
+
mutations: state.mutations,
|
|
111
|
+
error: { code: "EXTENSION_PRODUCT_CONFIG_REQUIRED", message, userAction: userAction2 }
|
|
112
|
+
});
|
|
113
|
+
return EXIT.infra;
|
|
114
|
+
}
|
|
115
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
116
|
+
deps.onRecovery?.(recoveryCode);
|
|
117
|
+
if (!json)
|
|
118
|
+
console.error(
|
|
119
|
+
`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`
|
|
120
|
+
);
|
|
121
|
+
const launch = deps.launch ?? (async (requestedTier) => {
|
|
122
|
+
const { launchExtension } = await import("../../commands/launch/extension.js");
|
|
123
|
+
const { readExtensionPinnedFlags } = await import("./feature-flags.js");
|
|
124
|
+
const { fixtureFeatureFlags, remoteFlagRequestFor } = await import("../../feature-flags.js");
|
|
125
|
+
const { walletFixturePath } = await import("../../paths.js");
|
|
126
|
+
return launchExtension(
|
|
127
|
+
target,
|
|
128
|
+
requestedTier,
|
|
129
|
+
false,
|
|
130
|
+
"fullscreen",
|
|
131
|
+
remoteFlagRequestFor(readExtensionPinnedFlags(target).pinned ?? fixtureFeatureFlags(walletFixturePath(target)))
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
const result = await launch(tier);
|
|
135
|
+
const verified = result.status === 0 ? await decide() : initial;
|
|
136
|
+
if (result.status !== 0 || verified.decision !== "ready") {
|
|
137
|
+
const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
|
|
138
|
+
emitProofFailure(json, {
|
|
139
|
+
recoverable: false,
|
|
140
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
141
|
+
error: { code: "EXTENSION_RUNTIME_RECOVERY_FAILED", message, userAction, originalError: result.output }
|
|
142
|
+
});
|
|
143
|
+
return EXIT.infra;
|
|
144
|
+
}
|
|
145
|
+
state.recovered.push(recoveryCode);
|
|
146
|
+
state.mutations.push({
|
|
147
|
+
type: "runtime",
|
|
148
|
+
action: tier === "build" ? "rebuilt" : "reloaded",
|
|
149
|
+
reasonCode: initial.reasonCode
|
|
150
|
+
});
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
71
153
|
const extensionRecording = {
|
|
72
154
|
target: (context) => resolveExtensionRecordingTarget(context),
|
|
73
155
|
framed: {
|
|
@@ -99,19 +181,21 @@ const extensionDiagnostics = {
|
|
|
99
181
|
};
|
|
100
182
|
const extensionObservation = {
|
|
101
183
|
network: {
|
|
102
|
-
backend: async (_target, env, artifactsDir) => (await import("
|
|
184
|
+
backend: async (_target, env, artifactsDir) => (await import("./network-observer.js")).createExtensionNetworkBackend(env, artifactsDir),
|
|
103
185
|
actions: true
|
|
104
186
|
},
|
|
105
187
|
performance: {
|
|
106
|
-
|
|
107
|
-
|
|
188
|
+
start: async (context) => {
|
|
189
|
+
const observation = await import("../../performance-observation.js");
|
|
190
|
+
return observation.startRunPerformanceObservation({
|
|
191
|
+
backend: (target, env) => observation.createExtensionPerformanceObserver(target, env),
|
|
192
|
+
platform: () => "extension"
|
|
193
|
+
}, context);
|
|
194
|
+
}
|
|
108
195
|
}
|
|
109
196
|
};
|
|
110
|
-
function restoreEnv(key, value) {
|
|
111
|
-
if (value === void 0) delete process.env[key];
|
|
112
|
-
else process.env[key] = value;
|
|
113
|
-
}
|
|
114
197
|
export {
|
|
198
|
+
ensureExtensionProofRuntime,
|
|
115
199
|
extensionDiagnostics,
|
|
116
200
|
extensionObservation,
|
|
117
201
|
extensionRecording,
|